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
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var events = require('events');
|
|
4
|
+
var index = require('./index.cjs');
|
|
5
|
+
var utils = require('../utils.cjs');
|
|
6
|
+
var promiseUtils = require('../../basics/promiseUtils.cjs');
|
|
7
|
+
var objChecker = require('../../basics/objChecker.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('../../basics/mediaContent.mjs').PictureDataType} PictureDataType
|
|
11
|
+
* @typedef {import('../../basics/mediaContent.mjs').MediaContent<PictureDataType>} MediaContent
|
|
12
|
+
* @typedef {import('./index.mjs').ContentTimeData} ContentTimeData
|
|
13
|
+
* @typedef {import('./index.mjs').ContentData} ContentData
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerState} YTPlayerState
|
|
16
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerErrors} YTPlayerErrors
|
|
17
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTQuality} YTQuality
|
|
18
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTSphericalProperties} YTSphericalProperties
|
|
19
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YouTubePlayerEventBase} YouTubePlayerEventBase
|
|
20
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerOptionsEvents} YTPlayerOptionsEvents
|
|
21
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerOptionsEvents} YTPlayerOptions
|
|
22
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').EventNames} EventNames
|
|
23
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlaylistOptions} YTPlaylistOptions
|
|
24
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').VideoByUrlOptions} VideoByUrlOptions
|
|
25
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').VideoByIdOptions} VideoByIdOptions
|
|
26
|
+
*
|
|
27
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnReadyEvent} OnReadyEvent
|
|
28
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnStateChangeEvent} OnStateChangeEvent
|
|
29
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnErrorEvent} OnErrorEvent
|
|
30
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnPlaybackQualityChangeEvent} OnPlaybackQualityChangeEvent
|
|
31
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnPlaybackRateChangeEvent} OnPlaybackRateChangeEvent
|
|
32
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnApiChangeEvent} OnApiChangeEvent
|
|
33
|
+
* @typedef {import('./docs/YouTubePlayer.mjs').OnAutoplayBlockedEvent} OnAutoplayBlockedEvent
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const makeLoadYoutubeApi = () => {
|
|
38
|
+
const { callback } = promiseUtils.createSingletonTask(async () => {
|
|
39
|
+
// Use existing API if already loaded
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
if (typeof window !== 'undefined' && window.YT && window.YT.Player) {
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
YoutubeMediaAdapter.PlayerState = window.YT.PlayerState;
|
|
44
|
+
return Promise.resolve(undefined);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
let isError = false;
|
|
49
|
+
const tag = document.createElement('script');
|
|
50
|
+
tag.src = 'https://www.youtube.com/iframe_api';
|
|
51
|
+
tag.async = true;
|
|
52
|
+
tag.onerror = () => {
|
|
53
|
+
isError = true;
|
|
54
|
+
reject(new Error('Failed to load YouTube IFrame API.'));
|
|
55
|
+
};
|
|
56
|
+
document.body.appendChild(tag);
|
|
57
|
+
|
|
58
|
+
// Check periodically if YT is ready
|
|
59
|
+
promiseUtils.waitForTrue(
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
() => isError || (typeof window !== 'undefined' && window.YT && window.YT.Player),
|
|
62
|
+
).then(() => {
|
|
63
|
+
if (isError) return;
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
YoutubeMediaAdapter.PlayerState = window.YT.PlayerState;
|
|
66
|
+
resolve(undefined);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return callback;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Method to ensure the YouTube IFrame API script is loaded.
|
|
75
|
+
*/
|
|
76
|
+
const loadYoutubeApi = makeLoadYoutubeApi();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a deep clone of the current YouTube player state values.
|
|
80
|
+
* This ensures the returned object is a copy and does not maintain a reference to the original state.
|
|
81
|
+
* @returns {Record<string, number>} A copy of the PlayerState object.
|
|
82
|
+
*/
|
|
83
|
+
const getPlayerStateValues = () => {
|
|
84
|
+
return structuredClone(YoutubeMediaAdapter.PlayerState);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Retrieves the YouTube Player constructor from the global window object.
|
|
89
|
+
* @returns {typeof YouTubePlayer} The YouTube Player constructor.
|
|
90
|
+
* @throws {Error} If the YouTube API is not detected in the global scope.
|
|
91
|
+
*/
|
|
92
|
+
const getYtPlayer = () => {
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
if (typeof window === 'undefined' || !window.YT || !window.YT.Player)
|
|
95
|
+
throw new Error('YouTube API not available');
|
|
96
|
+
/** @type {typeof YouTubePlayer} */
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
const Player = window.YT.Player;
|
|
99
|
+
return Player;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const checkDestroy = utils.createCheckDestroyed('YoutubeMediaAdapter');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Implementation of BaseMediaAdapter for the YouTube IFrame Player API.
|
|
106
|
+
* This adapter manages the lifecycle of a YouTube player embedded in a container.
|
|
107
|
+
* @extends BaseMediaAdapter
|
|
108
|
+
*/
|
|
109
|
+
class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
110
|
+
/**
|
|
111
|
+
* Mapping YouTube API events for internal adapter events.
|
|
112
|
+
* Includes semantic events triggered by state changes.
|
|
113
|
+
* @type {Record<string, string>}
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
static EVENT_MAPPING = {
|
|
117
|
+
onReady: 'onReady',
|
|
118
|
+
onPlaybackQualityChange: 'onPlaybackQualityChange',
|
|
119
|
+
onPlaybackRateChange: 'onPlaybackRateChange',
|
|
120
|
+
onError: 'onError',
|
|
121
|
+
onApiChange: 'onApiChange',
|
|
122
|
+
onAutoplayBlocked: 'onAutoplayBlocked',
|
|
123
|
+
onStateChange: 'onStateChange',
|
|
124
|
+
timeupdate: 'timeupdate',
|
|
125
|
+
play: 'play',
|
|
126
|
+
pause: 'pause',
|
|
127
|
+
ended: 'ended',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Represents the current state of the player.
|
|
132
|
+
* @type {Record<string, number>}
|
|
133
|
+
*/
|
|
134
|
+
static PlayerState = {
|
|
135
|
+
UNSTARTED: -1,
|
|
136
|
+
ENDED: 0,
|
|
137
|
+
PLAYING: 1,
|
|
138
|
+
PAUSED: 2,
|
|
139
|
+
BUFFERING: 3,
|
|
140
|
+
CUED: 5,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Safety lock: If true, multiple instances can share the same player/iframe via WeakMap.
|
|
145
|
+
* If false, only one instance can be bound to a specific container at a time.
|
|
146
|
+
* @type {boolean}
|
|
147
|
+
*/
|
|
148
|
+
static allowInstanceSharing = false;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The default HTML element where the YouTube iframe will be injected.
|
|
152
|
+
* @type {HTMLIFrameElement|null}
|
|
153
|
+
*/
|
|
154
|
+
static #defaultContainer = null;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Registry to reuse players and master emitters per container.
|
|
158
|
+
* @type {WeakMap<HTMLIFrameElement, {player: YouTubePlayer, masterEmitter: EventEmitter, refCount: number}>}
|
|
159
|
+
*/
|
|
160
|
+
static #registry = new WeakMap();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets the globally configured default container for YouTube players.
|
|
164
|
+
* @returns {HTMLIFrameElement|null}
|
|
165
|
+
*/
|
|
166
|
+
static get defaultContainer() {
|
|
167
|
+
return this.#defaultContainer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sets the globally configured default container for YouTube players.
|
|
172
|
+
* @param {HTMLIFrameElement|null} element - The element to be used as the default container.
|
|
173
|
+
* @throws {TypeError} If the provided element is not an instance of HTMLIFrameElement.
|
|
174
|
+
*/
|
|
175
|
+
static set defaultContainer(element) {
|
|
176
|
+
if (element !== null && !(element instanceof HTMLIFrameElement)) {
|
|
177
|
+
throw new TypeError('The defaultContainer must be an instance of HTMLIFrameElement.');
|
|
178
|
+
}
|
|
179
|
+
this.#defaultContainer = element;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a YouTube iframe element.
|
|
184
|
+
* This element can be used as the `defaultContainer` for the adapter.
|
|
185
|
+
*
|
|
186
|
+
* @param {IframeContainerOptions} options - Configuration for the iframe creation.
|
|
187
|
+
* @returns {HTMLIFrameElement} The created iframe element.
|
|
188
|
+
* @throws {TypeError} If the `videoId` is not a valid string.
|
|
189
|
+
*/
|
|
190
|
+
static createIframeContainer(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;
|
|
215
|
+
|
|
216
|
+
if (typeof videoId !== 'string' || videoId.trim() === '') {
|
|
217
|
+
throw new TypeError('The "videoId" property is required and must be a valid string.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const iframe = document.createElement('iframe');
|
|
221
|
+
|
|
222
|
+
// enablejsapi=1 is required to allow control via the YouTube API
|
|
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`;
|
|
224
|
+
|
|
225
|
+
iframe.style.width = `${width}px`;
|
|
226
|
+
iframe.style.height = `${height}px`;
|
|
227
|
+
iframe.setAttribute('frameborder', '0');
|
|
228
|
+
iframe.setAttribute('allow', 'autoplay; encrypted-media');
|
|
229
|
+
|
|
230
|
+
if (hidden) {
|
|
231
|
+
iframe.style.opacity = '0';
|
|
232
|
+
iframe.style.pointerEvents = 'none';
|
|
233
|
+
// Absolute positioning outside the viewport to prevent layout shifting
|
|
234
|
+
iframe.style.position = 'absolute';
|
|
235
|
+
iframe.style.top = '-9999px';
|
|
236
|
+
iframe.style.left = '-9999px';
|
|
237
|
+
document.body.appendChild(iframe);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return iframe;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Returns the URL for a YouTube video thumbnail with a specified quality.
|
|
245
|
+
* @param {string} videoId - The YouTube video ID.
|
|
246
|
+
* @param {string} [quality='maxresdefault'] - The thumbnail quality (e.g., 'default', 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault').
|
|
247
|
+
* @returns {string} The thumbnail URL.
|
|
248
|
+
* @throws {TypeError} If the videoId is not a valid string.
|
|
249
|
+
*/
|
|
250
|
+
static getThumbnailUrl(videoId, quality = 'maxresdefault') {
|
|
251
|
+
if (typeof videoId !== 'string' || videoId.trim() === '') {
|
|
252
|
+
throw new TypeError('The "videoId" must be a non-empty string.');
|
|
253
|
+
}
|
|
254
|
+
if (typeof quality !== 'string' || quality.trim() === '') {
|
|
255
|
+
throw new TypeError('The "quality" must be a non-empty string.');
|
|
256
|
+
}
|
|
257
|
+
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get id() {
|
|
261
|
+
return 'youtubeEmbedApi';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @type {YouTubePlayer|null} */
|
|
265
|
+
#player = null;
|
|
266
|
+
|
|
267
|
+
/** @type {HTMLIFrameElement} */
|
|
268
|
+
#container;
|
|
269
|
+
|
|
270
|
+
#isReady = false;
|
|
271
|
+
|
|
272
|
+
/** @type {Promise<void>} */
|
|
273
|
+
#apiLoadedPromise;
|
|
274
|
+
|
|
275
|
+
/** @type {number} */
|
|
276
|
+
#currentVolume = 1.0;
|
|
277
|
+
|
|
278
|
+
/** @type {EventEmitter|null} */
|
|
279
|
+
#masterEmitter = null;
|
|
280
|
+
|
|
281
|
+
/** @type {Array<{eventName: string, handler: HandlerFunc}>} */
|
|
282
|
+
#eventHandlers = [];
|
|
283
|
+
|
|
284
|
+
/** @type {boolean} */
|
|
285
|
+
#destroyed = false;
|
|
286
|
+
|
|
287
|
+
/** @type {string|null} */
|
|
288
|
+
#currentContentId = null;
|
|
289
|
+
|
|
290
|
+
get currentContentId() {
|
|
291
|
+
return this.#currentContentId;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @type {number} */
|
|
295
|
+
#lastReportedTime = 0;
|
|
296
|
+
|
|
297
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
298
|
+
#timeUpdateInterval = null;
|
|
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
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Gets whether the adapter has been destroyed.
|
|
316
|
+
* @returns {boolean}
|
|
317
|
+
*/
|
|
318
|
+
get destroyed() {
|
|
319
|
+
return this.#destroyed;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Initializes the YouTube Media Adapter.
|
|
324
|
+
* @throws {Error} If allowInstanceSharing is false and the container is already in use.
|
|
325
|
+
*/
|
|
326
|
+
constructor() {
|
|
327
|
+
const container = YoutubeMediaAdapter.#defaultContainer;
|
|
328
|
+
if (!(container instanceof HTMLIFrameElement)) {
|
|
329
|
+
throw new TypeError(
|
|
330
|
+
'The YoutubeMediaAdapter.defaultContainer must be an instance of HTMLIFrameElement.',
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Safety Lock Check
|
|
335
|
+
if (!YoutubeMediaAdapter.allowInstanceSharing && YoutubeMediaAdapter.#registry.has(container)) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
'Security Lock: This container is already bound to another YoutubeMediaAdapter instance.',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
super();
|
|
342
|
+
this.#container = container;
|
|
343
|
+
this.#apiLoadedPromise = loadYoutubeApi();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @returns {YouTubePlayer} The current YouTube player instance.
|
|
348
|
+
* @throws {Error} If the player has not been initialized.
|
|
349
|
+
*/
|
|
350
|
+
get player() {
|
|
351
|
+
checkDestroy(this.#destroyed);
|
|
352
|
+
if (!this.#player) {
|
|
353
|
+
throw new Error('YouTube player instance not initialized. Ensure a valid video was played.');
|
|
354
|
+
}
|
|
355
|
+
return this.#player;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {number} value - The volume level from 0.0 to 1.0.
|
|
360
|
+
* @throws {RangeError} If the volume is not between 0.0 and 1.0.
|
|
361
|
+
*/
|
|
362
|
+
set volume(value) {
|
|
363
|
+
checkDestroy(this.#destroyed);
|
|
364
|
+
if (typeof value !== 'number' || value < 0 || value > 1) {
|
|
365
|
+
throw new RangeError('Volume must be a number between 0.0 and 1.0.');
|
|
366
|
+
}
|
|
367
|
+
this.#currentVolume = value;
|
|
368
|
+
if (this.#player && typeof this.#player.setVolume === 'function') {
|
|
369
|
+
this.#player.setVolume(value * 100);
|
|
370
|
+
}
|
|
371
|
+
this.emit('volumeChange', this.#currentVolume);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @returns {number} The current volume level from 0.0 to 1.0.
|
|
376
|
+
*/
|
|
377
|
+
get volume() {
|
|
378
|
+
checkDestroy(this.#destroyed);
|
|
379
|
+
return this.#currentVolume;
|
|
380
|
+
}
|
|
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
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Retrieves a consolidated object containing all time-related metrics for the current media content.
|
|
402
|
+
* @returns {ContentTimeData} An object containing total, current, remaining time, and playback percentage.
|
|
403
|
+
*/
|
|
404
|
+
getTimeData() {
|
|
405
|
+
const total = this.getTotalDuration();
|
|
406
|
+
const current = this.getCurrentTime();
|
|
407
|
+
const remaining = total > 0 ? total - current : 0;
|
|
408
|
+
const playbackPercentage = total > 0 ? (current / total) * 100 : 0;
|
|
409
|
+
return { total, current, remaining, playbackPercentage };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Determines if the content is a valid YouTube URL or ID.
|
|
414
|
+
* @param {MediaContent} content - The media content to evaluate.
|
|
415
|
+
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
416
|
+
*/
|
|
417
|
+
static canHandle(content) {
|
|
418
|
+
if (!objChecker.isValidObj(content)) {
|
|
419
|
+
throw new TypeError('Content must be a valid object.');
|
|
420
|
+
}
|
|
421
|
+
if (typeof content.url !== 'string') return false;
|
|
422
|
+
const youtubeRegex =
|
|
423
|
+
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
|
|
424
|
+
return youtubeRegex.test(content.url);
|
|
425
|
+
}
|
|
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
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Plays the media content. If the player is already initialized with a different video,
|
|
439
|
+
* it will load the new video automatically.
|
|
440
|
+
*
|
|
441
|
+
* Example: https://www.youtube.com/watch?v=XXXXXXXXXXX
|
|
442
|
+
* @param {MediaContent} content - The media content to play.
|
|
443
|
+
* @returns {Promise<void>}
|
|
444
|
+
* @throws {TypeError} If the content data is invalid.
|
|
445
|
+
*/
|
|
446
|
+
async play(content) {
|
|
447
|
+
checkDestroy(this.#destroyed);
|
|
448
|
+
await this.#apiLoadedPromise;
|
|
449
|
+
|
|
450
|
+
if (!objChecker.isValidObj(content) || !content.url) {
|
|
451
|
+
throw new TypeError('Invalid media content provided to play().');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const videoSource = typeof content.url === 'string' ? content.url : '';
|
|
455
|
+
const targetVideoId = this.#extractVideoId(videoSource);
|
|
456
|
+
|
|
457
|
+
if (!this.#player) {
|
|
458
|
+
await this.#initializePlayer(targetVideoId);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (this.#player && typeof this.#player.setVolume === 'function') {
|
|
462
|
+
this.#player.setVolume(this.#currentVolume * 100);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If the video is different, load the new one
|
|
466
|
+
if (this.#currentContentId !== targetVideoId) {
|
|
467
|
+
this.#currentContentId = targetVideoId;
|
|
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;
|
|
472
|
+
} else {
|
|
473
|
+
this.#player?.playVideo();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return Promise.resolve();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Initializes the YT.Player instance and sets up semantic event bridging.
|
|
481
|
+
* @param {string} videoId - The video ID to load.
|
|
482
|
+
* @returns {Promise<void>}
|
|
483
|
+
*/
|
|
484
|
+
async #initializePlayer(videoId) {
|
|
485
|
+
checkDestroy(this.#destroyed);
|
|
486
|
+
const existing = YoutubeMediaAdapter.#registry.get(this.#container);
|
|
487
|
+
|
|
488
|
+
if (existing) {
|
|
489
|
+
existing.refCount++; // Increment reference count for the shared player
|
|
490
|
+
this.#player = existing.player;
|
|
491
|
+
this.#masterEmitter = existing.masterEmitter;
|
|
492
|
+
this.#currentContentId = this.#player.getVideoData()?.video_id || null;
|
|
493
|
+
this.#attachToMaster();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Create new master emitter for this specific container
|
|
498
|
+
const masterEmitter = new events.EventEmitter();
|
|
499
|
+
|
|
500
|
+
// Map YouTube API events to the Master Emitter
|
|
501
|
+
/** @type {Record<string, HandlerFunc>} */
|
|
502
|
+
const playerEvents = {};
|
|
503
|
+
Object.entries(YoutubeMediaAdapter.EVENT_MAPPING).forEach(([ytEvent, internalEvent]) => {
|
|
504
|
+
playerEvents[ytEvent] = (...args) => {
|
|
505
|
+
const result = masterEmitter.emit(internalEvent, ...args);
|
|
506
|
+
// Bridge YouTube states to semantic events: play, pause, ended
|
|
507
|
+
if (ytEvent === 'onStateChange') {
|
|
508
|
+
const state = args[0];
|
|
509
|
+
if (objChecker.isValidObj(state)) {
|
|
510
|
+
if (state.data === YoutubeMediaAdapter.PlayerState.PLAYING) masterEmitter.emit('play');
|
|
511
|
+
if (state.data === YoutubeMediaAdapter.PlayerState.PAUSED) masterEmitter.emit('pause');
|
|
512
|
+
if (state.data === YoutubeMediaAdapter.PlayerState.ENDED) masterEmitter.emit('ended');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return new Promise((resolve) => {
|
|
520
|
+
const Player = getYtPlayer();
|
|
521
|
+
this.#player = new Player(this.#container, {
|
|
522
|
+
videoId: videoId,
|
|
523
|
+
playerVars: { playsinline: 1 },
|
|
524
|
+
events: playerEvents,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Register this container and its master emitter with initial refCount
|
|
528
|
+
YoutubeMediaAdapter.#registry.set(this.#container, {
|
|
529
|
+
player: this.#player,
|
|
530
|
+
masterEmitter: masterEmitter,
|
|
531
|
+
refCount: 1,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
this.#masterEmitter = masterEmitter;
|
|
535
|
+
|
|
536
|
+
// Resolve the initialization promise when the player is ready
|
|
537
|
+
this.#masterEmitter?.once('onReady', () => {
|
|
538
|
+
this.#lastReportedTime = this.getCurrentTime();
|
|
539
|
+
this.#startPollingTimeUpdate();
|
|
540
|
+
this.#isReady = true;
|
|
541
|
+
resolve();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
this.#attachToMaster();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
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.
|
|
551
|
+
*/
|
|
552
|
+
#startPollingTimeUpdate() {
|
|
553
|
+
this.#timeUpdateInterval = setInterval(() => {
|
|
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
|
+
|
|
568
|
+
this.#masterEmitter?.emit('timeupdate', this.getTimeData());
|
|
569
|
+
}
|
|
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);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Connects this instance to the master emitter of the shared player.
|
|
579
|
+
*/
|
|
580
|
+
#attachToMaster() {
|
|
581
|
+
checkDestroy(this.#destroyed);
|
|
582
|
+
if (!this.#masterEmitter) return;
|
|
583
|
+
|
|
584
|
+
for (const eventName of Object.values(YoutubeMediaAdapter.EVENT_MAPPING)) {
|
|
585
|
+
/** @type {HandlerFunc} */
|
|
586
|
+
const handler = (...args) => this.emit(eventName, ...args);
|
|
587
|
+
this.#masterEmitter.on(eventName, handler);
|
|
588
|
+
this.#eventHandlers.push({ eventName, handler });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Extracts the 11-character video ID from a URL or returns the string if it is already an ID.
|
|
594
|
+
* @param {string} url - The URL or ID.
|
|
595
|
+
* @returns {string} The extracted video ID.
|
|
596
|
+
*/
|
|
597
|
+
#extractVideoId(url) {
|
|
598
|
+
checkDestroy(this.#destroyed);
|
|
599
|
+
const regex =
|
|
600
|
+
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
|
|
601
|
+
const match = url.match(regex);
|
|
602
|
+
return match ? match[1] : url;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Pauses the current playback.
|
|
607
|
+
* @returns {Promise<void>}
|
|
608
|
+
*/
|
|
609
|
+
async pause() {
|
|
610
|
+
checkDestroy(this.#destroyed);
|
|
611
|
+
this.#player?.pauseVideo();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Stops the playback completely.
|
|
616
|
+
* @returns {Promise<void>}
|
|
617
|
+
*/
|
|
618
|
+
async stop() {
|
|
619
|
+
checkDestroy(this.#destroyed);
|
|
620
|
+
this.#player?.stopVideo();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Seeks to a specific time in the media timeline.
|
|
625
|
+
* @param {number} timeMs - The target time in milliseconds.
|
|
626
|
+
* @returns {Promise<void>}
|
|
627
|
+
* @throws {TypeError} If timeMs is not a number.
|
|
628
|
+
*/
|
|
629
|
+
async seek(timeMs) {
|
|
630
|
+
checkDestroy(this.#destroyed);
|
|
631
|
+
if (typeof timeMs !== 'number') throw new TypeError('Time must be a number.');
|
|
632
|
+
const targetTimeInSeconds = timeMs / 1000;
|
|
633
|
+
this.#player?.seekTo(targetTimeInSeconds, true);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Retrieves the current playback time.
|
|
638
|
+
* @returns {number} The current time in milliseconds.
|
|
639
|
+
*/
|
|
640
|
+
getCurrentTime() {
|
|
641
|
+
checkDestroy(this.#destroyed);
|
|
642
|
+
return this.#player ? this.#player.getCurrentTime() * 1000 : 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Gets the total duration of the YouTube video.
|
|
647
|
+
* @returns {number} The total duration in milliseconds.
|
|
648
|
+
*/
|
|
649
|
+
getTotalDuration() {
|
|
650
|
+
checkDestroy(this.#destroyed);
|
|
651
|
+
return this.#player ? Math.floor(this.#player.getDuration() * 1000) : 0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Gets the remaining time until the YouTube video ends.
|
|
656
|
+
* @returns {number} The remaining time in milliseconds.
|
|
657
|
+
*/
|
|
658
|
+
getRemainingTime() {
|
|
659
|
+
checkDestroy(this.#destroyed);
|
|
660
|
+
const total = this.getTotalDuration();
|
|
661
|
+
const current = this.getCurrentTime();
|
|
662
|
+
return total > 0 ? total - current : 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Gets the percentage of the YouTube video that has been played.
|
|
667
|
+
* @returns {number} The percentage from 0 to 100.
|
|
668
|
+
*/
|
|
669
|
+
getPlaybackPercentage() {
|
|
670
|
+
checkDestroy(this.#destroyed);
|
|
671
|
+
const total = this.getTotalDuration();
|
|
672
|
+
const current = this.getCurrentTime();
|
|
673
|
+
return total > 0 ? (current / total) * 100 : 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Sets the playback volume.
|
|
678
|
+
* @param {number} volume - The volume level from 0.0 to 1.0.
|
|
679
|
+
* @returns {void}
|
|
680
|
+
* @throws {RangeError} If volume is outside [0.0, 1.0].
|
|
681
|
+
*/
|
|
682
|
+
setVolume(volume) {
|
|
683
|
+
this.volume = volume;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Gets the playback volume.
|
|
688
|
+
* @returns {number}
|
|
689
|
+
* @throws {RangeError} If volume is outside [0.0, 1.0].
|
|
690
|
+
*/
|
|
691
|
+
getVolume() {
|
|
692
|
+
return this.volume;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Retrieves the thumbnail URL for the currently loaded video with a specified quality.
|
|
697
|
+
* @param {string} [quality='maxresdefault'] - The thumbnail quality.
|
|
698
|
+
* @returns {string|null} The thumbnail URL or null if no video is loaded.
|
|
699
|
+
*/
|
|
700
|
+
getThumbnailUrl(quality = 'maxresdefault') {
|
|
701
|
+
checkDestroy(this.#destroyed);
|
|
702
|
+
return this.#currentContentId
|
|
703
|
+
? YoutubeMediaAdapter.getThumbnailUrl(this.#currentContentId, quality)
|
|
704
|
+
: null;
|
|
705
|
+
}
|
|
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
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Cleans up the instance and disconnects from the shared player.
|
|
730
|
+
*/
|
|
731
|
+
destroy() {
|
|
732
|
+
if (this.#destroyed) return;
|
|
733
|
+
|
|
734
|
+
if (this.#timeUpdateInterval) {
|
|
735
|
+
clearInterval(this.#timeUpdateInterval);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 1. Detach from the master emitter
|
|
739
|
+
if (this.#masterEmitter && this.#eventHandlers.length > 0) {
|
|
740
|
+
for (const { eventName, handler } of this.#eventHandlers) {
|
|
741
|
+
this.#masterEmitter.off(eventName, handler);
|
|
742
|
+
}
|
|
743
|
+
this.#eventHandlers = [];
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 2. Handle Registry Cleanup (Reference Counting)
|
|
747
|
+
const existing = YoutubeMediaAdapter.#registry.get(this.#container);
|
|
748
|
+
if (existing) {
|
|
749
|
+
existing.refCount--;
|
|
750
|
+
// If this was the last instance using this player/container, clean up the registry
|
|
751
|
+
if (existing.refCount <= 0) {
|
|
752
|
+
YoutubeMediaAdapter.#registry.delete(this.#container);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
this.#player = null;
|
|
757
|
+
this.#masterEmitter = null;
|
|
758
|
+
this.#currentContentId = null;
|
|
759
|
+
this.#destroyed = true;
|
|
760
|
+
super.destroy();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Asynchronously ensures the YouTube IFrame API is loaded and returns
|
|
766
|
+
* the YouTube Player constructor and the current player state mapping.
|
|
767
|
+
*
|
|
768
|
+
* @returns {Promise<{ Player: typeof YouTubePlayer, PlayerState: Record<string, number> }>}
|
|
769
|
+
* An object containing the Player constructor and the current player state mapping.
|
|
770
|
+
*/
|
|
771
|
+
const getYT = async () => {
|
|
772
|
+
await loadYoutubeApi();
|
|
773
|
+
return {
|
|
774
|
+
Player: getYtPlayer(),
|
|
775
|
+
PlayerState: getPlayerStateValues(),
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
exports.YoutubeMediaAdapter = YoutubeMediaAdapter;
|
|
780
|
+
exports.getPlayerStateValues = getPlayerStateValues;
|
|
781
|
+
exports.getYT = getYT;
|
|
782
|
+
exports.getYtPlayer = getYtPlayer;
|
|
783
|
+
exports.loadYoutubeApi = loadYoutubeApi;
|