@smartimpact-it/modern-video-embed 2.0.1
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 +1205 -0
- package/dist/components/VimeoEmbed.d.ts +143 -0
- package/dist/components/VimeoEmbed.d.ts.map +1 -0
- package/dist/components/VimeoEmbed.js +1176 -0
- package/dist/components/VimeoEmbed.js.map +1 -0
- package/dist/components/VimeoEmbed.min.js +1 -0
- package/dist/components/YouTubeEmbed.d.ts +225 -0
- package/dist/components/YouTubeEmbed.d.ts.map +1 -0
- package/dist/components/YouTubeEmbed.js +1354 -0
- package/dist/components/YouTubeEmbed.js.map +1 -0
- package/dist/components/YouTubeEmbed.min.js +1 -0
- package/dist/css/components.css +349 -0
- package/dist/css/components.css.map +1 -0
- package/dist/css/components.min.css +1 -0
- package/dist/css/main.css +12210 -0
- package/dist/css/main.css.map +1 -0
- package/dist/css/main.min.css +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/vimeo-only.d.ts +7 -0
- package/dist/vimeo-only.d.ts.map +1 -0
- package/dist/vimeo-only.js +8 -0
- package/dist/vimeo-only.js.map +1 -0
- package/dist/vimeo-only.min.js +1 -0
- package/dist/youtube-only.d.ts +7 -0
- package/dist/youtube-only.d.ts.map +1 -0
- package/dist/youtube-only.js +8 -0
- package/dist/youtube-only.js.map +1 -0
- package/dist/youtube-only.min.js +1 -0
- package/package.json +75 -0
- package/src/components/VimeoEmbed.ts +1340 -0
- package/src/components/YouTubeEmbed.ts +1568 -0
- package/src/index.ts +3 -0
- package/src/styles/README.md +56 -0
- package/src/styles/components.scss +7 -0
- package/src/styles/main.scss +10 -0
- package/src/styles/vimeo-embed.scss +255 -0
- package/src/styles/youtube-embed.scss +261 -0
- package/src/types/common.d.ts +198 -0
- package/src/types/index.ts +7 -0
- package/src/types/vimeo-embed.d.ts +80 -0
- package/src/types/youtube-embed.d.ts +83 -0
- package/src/vimeo-only.ts +9 -0
- package/src/youtube-only.ts +9 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
export class VimeoEmbed extends HTMLElement {
|
|
2
|
+
private iframe: HTMLIFrameElement | null = null;
|
|
3
|
+
private player: any = null; // Vimeo Player instance
|
|
4
|
+
private static apiLoaded = false;
|
|
5
|
+
private static apiReady = false;
|
|
6
|
+
private static instanceCount = 0;
|
|
7
|
+
private static DEBUG = false;
|
|
8
|
+
private playerReady = false;
|
|
9
|
+
private playPauseButton: HTMLDivElement | null = null;
|
|
10
|
+
private initialized = false;
|
|
11
|
+
private setCustomControlState: ((playing: boolean) => void) | null = null;
|
|
12
|
+
private updatingAttribute = false;
|
|
13
|
+
private updatingMutedState = false; // Flag to prevent sync from overwriting programmatic mute changes
|
|
14
|
+
|
|
15
|
+
/** The full Vimeo URL (e.g., "https://vimeo.com/...") */
|
|
16
|
+
#url: string = "";
|
|
17
|
+
/** The Vimeo video ID (numeric). */
|
|
18
|
+
#videoId: string = "";
|
|
19
|
+
/** Whether the video should autoplay when loaded. Requires `muted` to be true. */
|
|
20
|
+
#autoplay: boolean = false;
|
|
21
|
+
/** Whether the video audio is muted. */
|
|
22
|
+
#muted: boolean = false;
|
|
23
|
+
/** Whether to show the native Vimeo player controls. */
|
|
24
|
+
#controls: boolean = false;
|
|
25
|
+
/** Whether to lazy-load the video, showing a poster image until interaction. */
|
|
26
|
+
#lazy: boolean = false;
|
|
27
|
+
/** URL for a custom poster image. Defaults to Vimeo's thumbnail. */
|
|
28
|
+
#poster: string = "";
|
|
29
|
+
/** Read-only property to check if the video is currently playing. */
|
|
30
|
+
#playing: boolean = false;
|
|
31
|
+
/** Whether the video should act as a background, covering its container. */
|
|
32
|
+
#background: boolean = false;
|
|
33
|
+
/** Extra parameters to pass to the Vimeo player. */
|
|
34
|
+
#playerVars: Record<string, string | number | boolean> = {};
|
|
35
|
+
|
|
36
|
+
private posterClickHandler: EventListener | null = null;
|
|
37
|
+
private keyboardHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
38
|
+
private ariaLiveRegion: HTMLElement | null = null;
|
|
39
|
+
private apiLoadRetries = 0;
|
|
40
|
+
private static readonly MAX_API_RETRIES = 3;
|
|
41
|
+
private static readonly API_RETRY_DELAY = 2000;
|
|
42
|
+
private static readonly API_LOAD_TIMEOUT = 10000;
|
|
43
|
+
private intersectionObserver: IntersectionObserver | null = null;
|
|
44
|
+
private hasLoadedVideo = false;
|
|
45
|
+
|
|
46
|
+
static get observedAttributes() {
|
|
47
|
+
return [
|
|
48
|
+
"url",
|
|
49
|
+
"video-id",
|
|
50
|
+
"autoplay",
|
|
51
|
+
"controls",
|
|
52
|
+
"lazy",
|
|
53
|
+
"muted",
|
|
54
|
+
"poster",
|
|
55
|
+
"background",
|
|
56
|
+
"player-vars",
|
|
57
|
+
"quality",
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
attributeChangedCallback(
|
|
62
|
+
name: string,
|
|
63
|
+
oldValue: string | null,
|
|
64
|
+
newValue: string | null
|
|
65
|
+
) {
|
|
66
|
+
if (this.updatingAttribute) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.log(
|
|
71
|
+
`VimeoEmbed: Attribute changed - ${name}: ${oldValue} -> ${newValue}`
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
switch (name) {
|
|
75
|
+
case "url":
|
|
76
|
+
this.#url = newValue || "";
|
|
77
|
+
this.#videoId = this.extractVideoId(this.#url);
|
|
78
|
+
if (this.initialized) {
|
|
79
|
+
this.reinitializePlayer();
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case "video-id":
|
|
83
|
+
this.#videoId = newValue || "";
|
|
84
|
+
if (this.initialized) {
|
|
85
|
+
this.reinitializePlayer();
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case "autoplay":
|
|
89
|
+
this.#autoplay = newValue !== null;
|
|
90
|
+
if (
|
|
91
|
+
!this.#playing &&
|
|
92
|
+
this.#autoplay &&
|
|
93
|
+
!this.#lazy &&
|
|
94
|
+
this.initialized
|
|
95
|
+
) {
|
|
96
|
+
this.play();
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "controls":
|
|
100
|
+
this.#controls = newValue !== null;
|
|
101
|
+
if (this.initialized) {
|
|
102
|
+
this.reinitializePlayer();
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "lazy":
|
|
106
|
+
this.#lazy = newValue !== null;
|
|
107
|
+
break;
|
|
108
|
+
case "muted":
|
|
109
|
+
this.#muted = newValue !== null;
|
|
110
|
+
if (this.player && this.playerReady) {
|
|
111
|
+
this.player.setVolume(this.#muted ? 0 : 1);
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case "poster":
|
|
115
|
+
this.#poster = newValue || "";
|
|
116
|
+
if (this.#lazy && !this.player) {
|
|
117
|
+
this.showPoster(this.#videoId);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case "background":
|
|
121
|
+
this.#background = newValue !== null;
|
|
122
|
+
this.#updateBackgroundMode();
|
|
123
|
+
break;
|
|
124
|
+
case "player-vars":
|
|
125
|
+
try {
|
|
126
|
+
this.#playerVars = newValue ? JSON.parse(newValue) : {};
|
|
127
|
+
} catch (e) {
|
|
128
|
+
this.error("VimeoEmbed: Invalid player-vars JSON:", e);
|
|
129
|
+
this.#playerVars = {};
|
|
130
|
+
}
|
|
131
|
+
if (this.initialized) {
|
|
132
|
+
this.reinitializePlayer();
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case "quality":
|
|
136
|
+
if (newValue && this.player && this.playerReady) {
|
|
137
|
+
this.setQuality(newValue);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Getters and setters for attributes
|
|
144
|
+
get url() {
|
|
145
|
+
return this.#url;
|
|
146
|
+
}
|
|
147
|
+
set url(value: string) {
|
|
148
|
+
this.#url = value;
|
|
149
|
+
this.#videoId = this.extractVideoId(value);
|
|
150
|
+
if (this.initialized) {
|
|
151
|
+
this.reinitializePlayer();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get videoId() {
|
|
156
|
+
return this.#videoId;
|
|
157
|
+
}
|
|
158
|
+
set videoId(value: string) {
|
|
159
|
+
this.#videoId = value;
|
|
160
|
+
if (this.initialized) {
|
|
161
|
+
this.reinitializePlayer();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get autoplay() {
|
|
166
|
+
return this.#autoplay;
|
|
167
|
+
}
|
|
168
|
+
set autoplay(value: boolean) {
|
|
169
|
+
this.#autoplay = value;
|
|
170
|
+
this.#reflectBooleanAttribute("autoplay", value);
|
|
171
|
+
if (!this.#playing && value && !this.#lazy && this.initialized) {
|
|
172
|
+
this.play();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get controls() {
|
|
177
|
+
return this.#controls;
|
|
178
|
+
}
|
|
179
|
+
set controls(value: boolean) {
|
|
180
|
+
this.#controls = value;
|
|
181
|
+
this.#reflectBooleanAttribute("controls", value);
|
|
182
|
+
if (this.initialized) {
|
|
183
|
+
this.reinitializePlayer();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get lazy() {
|
|
188
|
+
return this.#lazy;
|
|
189
|
+
}
|
|
190
|
+
set lazy(value: boolean) {
|
|
191
|
+
this.#lazy = value;
|
|
192
|
+
this.#reflectBooleanAttribute("lazy", value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get muted() {
|
|
196
|
+
return this.#muted;
|
|
197
|
+
}
|
|
198
|
+
set muted(value: boolean) {
|
|
199
|
+
this.#muted = value;
|
|
200
|
+
this.#reflectBooleanAttribute("muted", value);
|
|
201
|
+
if (this.player && this.playerReady) {
|
|
202
|
+
this.updatingMutedState = true;
|
|
203
|
+
this.player.setVolume(value ? 0 : 1);
|
|
204
|
+
// Reset flag after a short delay to allow Vimeo API to process
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
this.updatingMutedState = false;
|
|
207
|
+
}, 100);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get poster() {
|
|
212
|
+
return this.#poster;
|
|
213
|
+
}
|
|
214
|
+
set poster(value: string) {
|
|
215
|
+
this.#poster = value;
|
|
216
|
+
this.updatingAttribute = true;
|
|
217
|
+
if (value) {
|
|
218
|
+
this.setAttribute("poster", value);
|
|
219
|
+
} else {
|
|
220
|
+
this.removeAttribute("poster");
|
|
221
|
+
}
|
|
222
|
+
this.updatingAttribute = false;
|
|
223
|
+
if (this.#lazy && !this.player) {
|
|
224
|
+
this.showPoster(this.#videoId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get playing() {
|
|
229
|
+
return this.#playing;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get background() {
|
|
233
|
+
return this.#background;
|
|
234
|
+
}
|
|
235
|
+
set background(value: boolean) {
|
|
236
|
+
this.#background = value;
|
|
237
|
+
this.#reflectBooleanAttribute("background", value);
|
|
238
|
+
this.#updateBackgroundMode();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get playerVars() {
|
|
242
|
+
return this.#playerVars;
|
|
243
|
+
}
|
|
244
|
+
set playerVars(vars: Record<string, string | number | boolean>) {
|
|
245
|
+
this.#playerVars = vars;
|
|
246
|
+
this.setAttribute("player-vars", JSON.stringify(vars));
|
|
247
|
+
if (this.initialized) {
|
|
248
|
+
this.reinitializePlayer();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#reflectBooleanAttribute(name: string, value: boolean) {
|
|
253
|
+
this.updatingAttribute = true;
|
|
254
|
+
if (value) {
|
|
255
|
+
this.setAttribute(name, "");
|
|
256
|
+
} else {
|
|
257
|
+
this.removeAttribute(name);
|
|
258
|
+
}
|
|
259
|
+
this.updatingAttribute = false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private extractVideoId(url: string): string {
|
|
263
|
+
if (!url) return "";
|
|
264
|
+
|
|
265
|
+
// Handle various Vimeo URL formats
|
|
266
|
+
// https://vimeo.com/123456789
|
|
267
|
+
// https://player.vimeo.com/video/123456789
|
|
268
|
+
const regex =
|
|
269
|
+
/(?:vimeo\.com\/(?:video\/)?|player\.vimeo\.com\/video\/)(\d+)/;
|
|
270
|
+
const match = url.match(regex);
|
|
271
|
+
|
|
272
|
+
if (match && match[1]) {
|
|
273
|
+
return match[1];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If it's already just an ID (numeric)
|
|
277
|
+
if (/^\d+$/.test(url)) {
|
|
278
|
+
return url;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.warn(`VimeoEmbed: Could not extract video ID from "${url}"`);
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private log(...args: any[]) {
|
|
286
|
+
if (VimeoEmbed.DEBUG) {
|
|
287
|
+
console.log("VimeoEmbed:", ...args);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private warn(...args: any[]) {
|
|
292
|
+
console.warn(...args);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private error(...args: any[]) {
|
|
296
|
+
console.error(...args);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private dispatchCustomEvent(eventName: string, detail?: any) {
|
|
300
|
+
this.dispatchEvent(
|
|
301
|
+
new CustomEvent(eventName, {
|
|
302
|
+
detail,
|
|
303
|
+
bubbles: true,
|
|
304
|
+
composed: true,
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private isValidPosterUrl(url: string): boolean {
|
|
310
|
+
if (!url) return false;
|
|
311
|
+
try {
|
|
312
|
+
new URL(url);
|
|
313
|
+
return true;
|
|
314
|
+
} catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Setup fullscreen change event listener
|
|
321
|
+
*/
|
|
322
|
+
private setupFullscreenListener(): void {
|
|
323
|
+
const handleFullscreenChange = () => {
|
|
324
|
+
const isFullscreen = this.isFullscreen();
|
|
325
|
+
this.dispatchCustomEvent("fullscreenchange", { isFullscreen });
|
|
326
|
+
|
|
327
|
+
if (isFullscreen) {
|
|
328
|
+
this.classList.add("is-fullscreen");
|
|
329
|
+
} else {
|
|
330
|
+
this.classList.remove("is-fullscreen");
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
335
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
336
|
+
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
337
|
+
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Setup keyboard event handlers for accessibility
|
|
342
|
+
*/
|
|
343
|
+
private setupKeyboardHandlers(): void {
|
|
344
|
+
this.keyboardHandler = (e: KeyboardEvent) => {
|
|
345
|
+
// Only handle if player is ready and not in input element
|
|
346
|
+
if (!this.playerReady || (e.target as HTMLElement)?.tagName === "INPUT") {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switch (e.key.toLowerCase()) {
|
|
351
|
+
case "k":
|
|
352
|
+
case " ":
|
|
353
|
+
// Play/Pause
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
this.togglePlay();
|
|
356
|
+
this.announceToScreenReader(
|
|
357
|
+
this.#playing ? "Video playing" : "Video paused"
|
|
358
|
+
);
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case "m":
|
|
362
|
+
// Mute/Unmute
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
this.toggleMute();
|
|
365
|
+
this.player?.getMuted().then((muted: boolean) => {
|
|
366
|
+
this.announceToScreenReader(
|
|
367
|
+
muted ? "Video muted" : "Video unmuted"
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case "f":
|
|
373
|
+
// Fullscreen toggle
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
if (document.fullscreenElement) {
|
|
376
|
+
document.exitFullscreen();
|
|
377
|
+
this.announceToScreenReader("Exited fullscreen");
|
|
378
|
+
} else if (this.enterFullscreen) {
|
|
379
|
+
this.enterFullscreen();
|
|
380
|
+
this.announceToScreenReader("Entered fullscreen");
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
this.addEventListener("keydown", this.keyboardHandler as EventListener);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Announce message to screen readers
|
|
391
|
+
*/
|
|
392
|
+
private announceToScreenReader(message: string): void {
|
|
393
|
+
if (this.ariaLiveRegion) {
|
|
394
|
+
this.ariaLiveRegion.textContent = message;
|
|
395
|
+
// Clear after a brief delay
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
if (this.ariaLiveRegion) {
|
|
398
|
+
this.ariaLiveRegion.textContent = "";
|
|
399
|
+
}
|
|
400
|
+
}, 1000);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Add preconnect and dns-prefetch hints for Vimeo domains
|
|
406
|
+
*/
|
|
407
|
+
private addResourceHints(): void {
|
|
408
|
+
const hints = [
|
|
409
|
+
{ rel: "preconnect", href: "https://player.vimeo.com" },
|
|
410
|
+
{ rel: "preconnect", href: "https://i.vimeocdn.com" },
|
|
411
|
+
{ rel: "dns-prefetch", href: "https://player.vimeo.com" },
|
|
412
|
+
{ rel: "dns-prefetch", href: "https://i.vimeocdn.com" },
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
hints.forEach(({ rel, href }) => {
|
|
416
|
+
if (!document.querySelector(`link[rel="${rel}"][href="${href}"]`)) {
|
|
417
|
+
const link = document.createElement("link");
|
|
418
|
+
link.rel = rel;
|
|
419
|
+
link.href = href;
|
|
420
|
+
if (rel === "preconnect") {
|
|
421
|
+
link.crossOrigin = "anonymous";
|
|
422
|
+
}
|
|
423
|
+
document.head.appendChild(link);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Setup Intersection Observer for lazy loading
|
|
430
|
+
*/
|
|
431
|
+
private setupIntersectionObserver(): void {
|
|
432
|
+
if (!("IntersectionObserver" in window)) {
|
|
433
|
+
// Fallback: load immediately if IntersectionObserver not supported
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const options: IntersectionObserverInit = {
|
|
438
|
+
root: null,
|
|
439
|
+
rootMargin: "50px", // Start loading 50px before entering viewport
|
|
440
|
+
threshold: 0.01,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
444
|
+
entries.forEach((entry) => {
|
|
445
|
+
if (entry.isIntersecting && !this.hasLoadedVideo) {
|
|
446
|
+
this.hasLoadedVideo = true;
|
|
447
|
+
this.log("VimeoEmbed: Video entering viewport, loading...");
|
|
448
|
+
|
|
449
|
+
// Check if poster was clicked before intersection
|
|
450
|
+
const shouldAutoplay =
|
|
451
|
+
this.getAttribute("data-poster-autoplay") === "true";
|
|
452
|
+
|
|
453
|
+
if (!shouldAutoplay) {
|
|
454
|
+
// Just load the player, don't autoplay
|
|
455
|
+
this.initializePlayer(this.#videoId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Disconnect observer after loading
|
|
459
|
+
if (this.intersectionObserver) {
|
|
460
|
+
this.intersectionObserver.disconnect();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}, options);
|
|
465
|
+
|
|
466
|
+
this.intersectionObserver.observe(this);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Display error message to user with retry option
|
|
471
|
+
*/
|
|
472
|
+
private showErrorMessage(message: string): void {
|
|
473
|
+
const errorDiv = document.createElement("div");
|
|
474
|
+
errorDiv.className = "vimeo-error-message";
|
|
475
|
+
errorDiv.setAttribute("role", "alert");
|
|
476
|
+
errorDiv.innerHTML = `
|
|
477
|
+
<div class="error-content">
|
|
478
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
479
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
480
|
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
481
|
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
482
|
+
</svg>
|
|
483
|
+
<p class="error-message">${message}</p>
|
|
484
|
+
<button class="retry-button">Retry</button>
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
const retryButton = errorDiv.querySelector(".retry-button");
|
|
489
|
+
if (retryButton) {
|
|
490
|
+
retryButton.addEventListener("click", () => {
|
|
491
|
+
this.apiLoadRetries = 0;
|
|
492
|
+
errorDiv.remove();
|
|
493
|
+
if (this.#videoId) {
|
|
494
|
+
this.initializePlayer(this.#videoId);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Clear existing content
|
|
500
|
+
this.innerHTML = "";
|
|
501
|
+
this.appendChild(errorDiv);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
connectedCallback() {
|
|
505
|
+
VimeoEmbed.instanceCount++;
|
|
506
|
+
this.log(
|
|
507
|
+
"VimeoEmbed: connectedCallback called, instance count:",
|
|
508
|
+
VimeoEmbed.instanceCount
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Add resource hints on first instance
|
|
512
|
+
if (VimeoEmbed.instanceCount === 1) {
|
|
513
|
+
this.addResourceHints();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const urlAttr = this.getAttribute("url");
|
|
517
|
+
const videoIdAttr = this.getAttribute("video-id");
|
|
518
|
+
|
|
519
|
+
if (urlAttr) {
|
|
520
|
+
this.#url = urlAttr;
|
|
521
|
+
this.#videoId = this.extractVideoId(urlAttr);
|
|
522
|
+
} else if (videoIdAttr) {
|
|
523
|
+
this.#videoId = videoIdAttr;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
this.#autoplay = this.hasAttribute("autoplay");
|
|
527
|
+
this.#controls = this.hasAttribute("controls");
|
|
528
|
+
this.#lazy = this.hasAttribute("lazy");
|
|
529
|
+
this.#muted = this.hasAttribute("muted");
|
|
530
|
+
this.#background = this.hasAttribute("background");
|
|
531
|
+
|
|
532
|
+
this.#poster = this.getAttribute("poster") || "";
|
|
533
|
+
try {
|
|
534
|
+
const playerVarsAttr = this.getAttribute("player-vars");
|
|
535
|
+
this.#playerVars = playerVarsAttr ? JSON.parse(playerVarsAttr) : {};
|
|
536
|
+
} catch (e) {
|
|
537
|
+
this.error("VimeoEmbed: Invalid player-vars JSON on init:", e);
|
|
538
|
+
this.#playerVars = {};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!this.#videoId) {
|
|
542
|
+
this.error("VimeoEmbed: Missing or invalid video ID");
|
|
543
|
+
this.dispatchCustomEvent("error", {
|
|
544
|
+
message: "Missing or invalid video ID",
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.classList.add("vimeo-embed-wrapper");
|
|
550
|
+
|
|
551
|
+
// Add ARIA role and tabindex for accessibility
|
|
552
|
+
this.setAttribute("role", "region");
|
|
553
|
+
this.setAttribute("aria-label", "Video player");
|
|
554
|
+
if (!this.hasAttribute("tabindex")) {
|
|
555
|
+
this.setAttribute("tabindex", "0");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Create ARIA live region for screen reader announcements
|
|
559
|
+
this.ariaLiveRegion = document.createElement("div");
|
|
560
|
+
this.ariaLiveRegion.setAttribute("aria-live", "polite");
|
|
561
|
+
this.ariaLiveRegion.setAttribute("aria-atomic", "true");
|
|
562
|
+
this.ariaLiveRegion.className = "sr-only";
|
|
563
|
+
this.ariaLiveRegion.style.cssText =
|
|
564
|
+
"position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;";
|
|
565
|
+
this.appendChild(this.ariaLiveRegion);
|
|
566
|
+
|
|
567
|
+
// Setup keyboard event handlers
|
|
568
|
+
this.setupKeyboardHandlers();
|
|
569
|
+
|
|
570
|
+
// Setup fullscreen change listener
|
|
571
|
+
this.setupFullscreenListener();
|
|
572
|
+
|
|
573
|
+
this.#updateBackgroundMode();
|
|
574
|
+
|
|
575
|
+
if (this.#lazy) {
|
|
576
|
+
this.showPoster(this.#videoId);
|
|
577
|
+
// Setup Intersection Observer for better performance
|
|
578
|
+
this.setupIntersectionObserver();
|
|
579
|
+
} else {
|
|
580
|
+
this.initializePlayer(this.#videoId);
|
|
581
|
+
}
|
|
582
|
+
this.initialized = true;
|
|
583
|
+
this.dispatchCustomEvent("connected");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
disconnectedCallback() {
|
|
587
|
+
VimeoEmbed.instanceCount--;
|
|
588
|
+
this.log(
|
|
589
|
+
"VimeoEmbed: disconnectedCallback called, remaining instances:",
|
|
590
|
+
VimeoEmbed.instanceCount
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (this.player) {
|
|
594
|
+
try {
|
|
595
|
+
this.player.destroy();
|
|
596
|
+
this.log("VimeoEmbed: Player destroyed.");
|
|
597
|
+
} catch (error) {
|
|
598
|
+
this.warn("VimeoEmbed: Error destroying player:", error);
|
|
599
|
+
}
|
|
600
|
+
this.player = null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Remove keyboard event handlers
|
|
604
|
+
if (this.keyboardHandler) {
|
|
605
|
+
this.removeEventListener(
|
|
606
|
+
"keydown",
|
|
607
|
+
this.keyboardHandler as EventListener
|
|
608
|
+
);
|
|
609
|
+
this.keyboardHandler = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const poster = this.querySelector(".vimeo-poster");
|
|
613
|
+
const buttonOverlay = this.querySelector(".button-overlay");
|
|
614
|
+
|
|
615
|
+
if (poster && this.posterClickHandler) {
|
|
616
|
+
poster.removeEventListener("click", this.posterClickHandler);
|
|
617
|
+
this.posterClickHandler = null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (buttonOverlay && this.posterClickHandler) {
|
|
621
|
+
buttonOverlay.removeEventListener("click", this.posterClickHandler);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this.innerHTML = "";
|
|
625
|
+
this.classList.remove(
|
|
626
|
+
"vimeo-embed-wrapper",
|
|
627
|
+
"vimeo-embed-container",
|
|
628
|
+
"is-playing"
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (VimeoEmbed.instanceCount === 0) {
|
|
632
|
+
this.log("VimeoEmbed: Last instance, cleaning up global resources");
|
|
633
|
+
|
|
634
|
+
const script = document.querySelector(
|
|
635
|
+
'script[src="https://player.vimeo.com/api/player.js"]'
|
|
636
|
+
);
|
|
637
|
+
if (script) {
|
|
638
|
+
script.remove();
|
|
639
|
+
this.log("VimeoEmbed: Vimeo API script removed.");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
VimeoEmbed.apiLoaded = false;
|
|
643
|
+
VimeoEmbed.apiReady = false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
this.iframe = null;
|
|
647
|
+
this.playPauseButton = null;
|
|
648
|
+
this.setCustomControlState = null;
|
|
649
|
+
|
|
650
|
+
this.dispatchCustomEvent("disconnected");
|
|
651
|
+
this.log("VimeoEmbed: Cleanup complete.");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private reinitializePlayer() {
|
|
655
|
+
if (!this.initialized || !this.#videoId) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (this.player) {
|
|
660
|
+
try {
|
|
661
|
+
this.player.destroy();
|
|
662
|
+
} catch (error) {
|
|
663
|
+
this.warn("VimeoEmbed: Error destroying player:", error);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (this.#lazy) {
|
|
668
|
+
this.showPoster(this.#videoId);
|
|
669
|
+
} else {
|
|
670
|
+
this.initializePlayer(this.#videoId);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private async showPoster(videoId: string) {
|
|
675
|
+
let thumbnailUrl = this.#poster;
|
|
676
|
+
|
|
677
|
+
// Fetch Vimeo thumbnail if no custom poster
|
|
678
|
+
if (!this.isValidPosterUrl(this.#poster)) {
|
|
679
|
+
try {
|
|
680
|
+
const response = await fetch(
|
|
681
|
+
`https://vimeo.com/api/v2/video/${videoId}.json`
|
|
682
|
+
);
|
|
683
|
+
const data = await response.json();
|
|
684
|
+
thumbnailUrl = data[0]?.thumbnail_large || "";
|
|
685
|
+
} catch (error) {
|
|
686
|
+
this.warn("VimeoEmbed: Could not fetch Vimeo thumbnail:", error);
|
|
687
|
+
thumbnailUrl = "";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
this.log("VimeoEmbed: Using poster URL:", thumbnailUrl);
|
|
692
|
+
|
|
693
|
+
this.innerHTML = "";
|
|
694
|
+
|
|
695
|
+
if (this.posterClickHandler) {
|
|
696
|
+
this.posterClickHandler = null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const poster = document.createElement("img");
|
|
700
|
+
poster.src = thumbnailUrl || "";
|
|
701
|
+
poster.alt = "Vimeo Video Thumbnail";
|
|
702
|
+
poster.classList.add("vimeo-poster", "video-poster");
|
|
703
|
+
poster.loading = "lazy";
|
|
704
|
+
|
|
705
|
+
poster.onerror = () => {
|
|
706
|
+
this.warn("VimeoEmbed: Poster failed to load, using white background.");
|
|
707
|
+
poster.style.display = "none";
|
|
708
|
+
this.style.backgroundColor = "#FFFFFF";
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const buttonOverlay = document.createElement("div");
|
|
712
|
+
buttonOverlay.classList.add("button-overlay");
|
|
713
|
+
buttonOverlay.setAttribute("role", "button");
|
|
714
|
+
buttonOverlay.setAttribute("tabindex", "0");
|
|
715
|
+
buttonOverlay.setAttribute("aria-label", "Play video");
|
|
716
|
+
|
|
717
|
+
const button = document.createElement("div");
|
|
718
|
+
button.classList.add("button");
|
|
719
|
+
button.setAttribute("aria-hidden", "true");
|
|
720
|
+
|
|
721
|
+
buttonOverlay.appendChild(button);
|
|
722
|
+
|
|
723
|
+
const loadVideo: EventListener = () => {
|
|
724
|
+
this.log("VimeoEmbed: Loading video from poster click");
|
|
725
|
+
this.setAttribute("data-poster-autoplay", "true");
|
|
726
|
+
try {
|
|
727
|
+
this.initializePlayer(videoId);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
this.error("VimeoEmbed: Error initializing player from poster:", error);
|
|
730
|
+
this.dispatchCustomEvent("error", { message: "Failed to load video" });
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
this.posterClickHandler = loadVideo;
|
|
735
|
+
|
|
736
|
+
// Add keyboard support
|
|
737
|
+
const keyboardActivate = (e: KeyboardEvent) => {
|
|
738
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
739
|
+
e.preventDefault();
|
|
740
|
+
loadVideo(e);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
poster.addEventListener("click", loadVideo);
|
|
745
|
+
buttonOverlay.addEventListener("click", loadVideo);
|
|
746
|
+
buttonOverlay.addEventListener(
|
|
747
|
+
"keydown",
|
|
748
|
+
keyboardActivate as EventListener
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
this.appendChild(poster);
|
|
752
|
+
this.appendChild(buttonOverlay);
|
|
753
|
+
|
|
754
|
+
this.log("VimeoEmbed: Poster displayed for lazy loading");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private async initializePlayer(videoId: string) {
|
|
758
|
+
this.log("VimeoEmbed: Initializing player for video ID:", videoId);
|
|
759
|
+
|
|
760
|
+
const isBackground = this.#background;
|
|
761
|
+
const autoplay =
|
|
762
|
+
isBackground ||
|
|
763
|
+
this.hasAttribute("autoplay") ||
|
|
764
|
+
this.hasAttribute("data-poster-autoplay") ||
|
|
765
|
+
this.hasAttribute("data-should-autoplay");
|
|
766
|
+
const controls = isBackground ? false : this.hasAttribute("controls");
|
|
767
|
+
const mute = isBackground || autoplay;
|
|
768
|
+
|
|
769
|
+
if (this.hasAttribute("data-poster-autoplay")) {
|
|
770
|
+
this.removeAttribute("data-poster-autoplay");
|
|
771
|
+
}
|
|
772
|
+
if (this.hasAttribute("data-should-autoplay")) {
|
|
773
|
+
this.removeAttribute("data-should-autoplay");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.log(
|
|
777
|
+
"VimeoEmbed: Creating iframe with autoplay:",
|
|
778
|
+
autoplay,
|
|
779
|
+
"controls:",
|
|
780
|
+
controls
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
this.iframe = document.createElement("iframe");
|
|
784
|
+
|
|
785
|
+
// Generate unique ID for Vimeo Player API
|
|
786
|
+
const iframeId = `vimeo-player-${videoId}-${Date.now()}`;
|
|
787
|
+
this.iframe.id = iframeId;
|
|
788
|
+
|
|
789
|
+
const params = new URLSearchParams({
|
|
790
|
+
autoplay: autoplay ? "1" : "0",
|
|
791
|
+
controls: controls ? "1" : "0",
|
|
792
|
+
muted: mute ? "1" : "0",
|
|
793
|
+
playsinline: "1",
|
|
794
|
+
dnt: "1", // Do not track
|
|
795
|
+
...this.#playerVars,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
if (this.#muted) {
|
|
799
|
+
params.set("muted", "1");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (autoplay && !this.#muted) {
|
|
803
|
+
this.#muted = true;
|
|
804
|
+
this.log(
|
|
805
|
+
"VimeoEmbed: Autoplay enabled, forcing muted state for compliance"
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.iframe.src = `https://player.vimeo.com/video/${videoId}?${params.toString()}`;
|
|
810
|
+
this.iframe.allow =
|
|
811
|
+
"autoplay; fullscreen; picture-in-picture; clipboard-write";
|
|
812
|
+
this.iframe.allowFullscreen = true;
|
|
813
|
+
this.iframe.style.position = "absolute";
|
|
814
|
+
this.iframe.style.border = "none";
|
|
815
|
+
|
|
816
|
+
if (this.#background) {
|
|
817
|
+
this.iframe.style.pointerEvents = "none";
|
|
818
|
+
} else {
|
|
819
|
+
this.iframe.style.top = "0";
|
|
820
|
+
this.iframe.style.left = "0";
|
|
821
|
+
this.iframe.style.width = "100%";
|
|
822
|
+
this.iframe.style.height = "100%";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this.log("VimeoEmbed: Iframe created with src:", this.iframe.src);
|
|
826
|
+
|
|
827
|
+
this.replaceChildren(this.iframe);
|
|
828
|
+
|
|
829
|
+
// Store reference to iframe before async operations
|
|
830
|
+
const iframe = this.iframe;
|
|
831
|
+
|
|
832
|
+
if (!controls) {
|
|
833
|
+
this.addCustomControls();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
await VimeoEmbed.loadVimeoAPIWithRetry(this.apiLoadRetries);
|
|
838
|
+
|
|
839
|
+
// Check if component was disconnected during API load
|
|
840
|
+
if (!this.isConnected || !iframe.isConnected) {
|
|
841
|
+
this.log(
|
|
842
|
+
"VimeoEmbed: Component disconnected, skipping Player initialization"
|
|
843
|
+
);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Wait for iframe to be fully in DOM before initializing player
|
|
848
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
849
|
+
|
|
850
|
+
// @ts-ignore - Vimeo Player will be available after loading the API
|
|
851
|
+
// Use the iframe element directly since it's already in the DOM
|
|
852
|
+
this.player = new Vimeo.Player(iframe);
|
|
853
|
+
|
|
854
|
+
this.player.on("loaded", () => {
|
|
855
|
+
this.log("VimeoEmbed: Player is ready.");
|
|
856
|
+
this.playerReady = true;
|
|
857
|
+
|
|
858
|
+
if (this.#muted) {
|
|
859
|
+
this.player.setVolume(0);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (autoplay && this.setCustomControlState) {
|
|
863
|
+
this.setCustomControlState(true);
|
|
864
|
+
this.#playing = true;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.dispatchCustomEvent("ready");
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
this.player.on("play", () => {
|
|
871
|
+
this.#playing = true;
|
|
872
|
+
if (this.setCustomControlState) {
|
|
873
|
+
this.setCustomControlState(true);
|
|
874
|
+
}
|
|
875
|
+
this.dispatchCustomEvent("play");
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
this.player.on("pause", () => {
|
|
879
|
+
this.#playing = false;
|
|
880
|
+
if (this.setCustomControlState) {
|
|
881
|
+
this.setCustomControlState(false);
|
|
882
|
+
}
|
|
883
|
+
this.dispatchCustomEvent("pause");
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
this.player.on("ended", () => {
|
|
887
|
+
this.#playing = false;
|
|
888
|
+
if (this.setCustomControlState) {
|
|
889
|
+
this.setCustomControlState(false);
|
|
890
|
+
}
|
|
891
|
+
this.dispatchCustomEvent("ended");
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
this.player.on("error", (error: any) => {
|
|
895
|
+
this.error("VimeoEmbed: Player encountered an error:", error);
|
|
896
|
+
this.dispatchCustomEvent("error", {
|
|
897
|
+
message: error.message || "Unknown error occurred",
|
|
898
|
+
name: error.name,
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
} catch (error) {
|
|
902
|
+
this.error("VimeoEmbed: Failed to load Vimeo API", error);
|
|
903
|
+
this.showErrorMessage(
|
|
904
|
+
error instanceof Error
|
|
905
|
+
? error.message
|
|
906
|
+
: "Failed to load Vimeo player. Please refresh the page or check your connection."
|
|
907
|
+
);
|
|
908
|
+
this.dispatchCustomEvent("error", {
|
|
909
|
+
message: error instanceof Error ? error.message : "API load failed",
|
|
910
|
+
code: "API_LOAD_ERROR",
|
|
911
|
+
retryable: true,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private static async loadVimeoAPI(): Promise<void> {
|
|
917
|
+
return new Promise((resolve, reject) => {
|
|
918
|
+
if (this.apiReady) {
|
|
919
|
+
resolve();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (!this.apiLoaded) {
|
|
924
|
+
const script = document.createElement("script");
|
|
925
|
+
script.src = "https://player.vimeo.com/api/player.js";
|
|
926
|
+
script.onload = () => {
|
|
927
|
+
this.apiReady = true;
|
|
928
|
+
this.apiLoaded = true;
|
|
929
|
+
resolve();
|
|
930
|
+
};
|
|
931
|
+
script.onerror = () => {
|
|
932
|
+
reject(
|
|
933
|
+
new Error(
|
|
934
|
+
"Failed to load Vimeo API script. Please check your network connection or disable ad blockers."
|
|
935
|
+
)
|
|
936
|
+
);
|
|
937
|
+
};
|
|
938
|
+
document.head.appendChild(script);
|
|
939
|
+
this.apiLoaded = true;
|
|
940
|
+
} else {
|
|
941
|
+
// Wait for API to be ready
|
|
942
|
+
const checkReady = setInterval(() => {
|
|
943
|
+
// @ts-ignore
|
|
944
|
+
if (typeof Vimeo !== "undefined") {
|
|
945
|
+
this.apiReady = true;
|
|
946
|
+
clearInterval(checkReady);
|
|
947
|
+
resolve();
|
|
948
|
+
}
|
|
949
|
+
}, 100);
|
|
950
|
+
|
|
951
|
+
setTimeout(() => {
|
|
952
|
+
clearInterval(checkReady);
|
|
953
|
+
reject(
|
|
954
|
+
new Error(
|
|
955
|
+
"Vimeo API loading timeout. The API script may be blocked by an ad blocker or network issue."
|
|
956
|
+
)
|
|
957
|
+
);
|
|
958
|
+
}, this.API_LOAD_TIMEOUT);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Load Vimeo API with retry mechanism
|
|
965
|
+
*/
|
|
966
|
+
private static async loadVimeoAPIWithRetry(retries = 0): Promise<void> {
|
|
967
|
+
try {
|
|
968
|
+
await this.loadVimeoAPI();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (retries < this.MAX_API_RETRIES) {
|
|
971
|
+
console.warn(
|
|
972
|
+
`VimeoEmbed: API load failed, retrying (${retries + 1}/${
|
|
973
|
+
this.MAX_API_RETRIES
|
|
974
|
+
})...`
|
|
975
|
+
);
|
|
976
|
+
await new Promise((resolve) =>
|
|
977
|
+
setTimeout(resolve, this.API_RETRY_DELAY)
|
|
978
|
+
);
|
|
979
|
+
return this.loadVimeoAPIWithRetry(retries + 1);
|
|
980
|
+
}
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private addCustomControls() {
|
|
986
|
+
this.classList.add("vimeo-embed-container");
|
|
987
|
+
|
|
988
|
+
const buttonOverlay = document.createElement("div");
|
|
989
|
+
buttonOverlay.classList.add("button-overlay");
|
|
990
|
+
|
|
991
|
+
this.playPauseButton = document.createElement("div");
|
|
992
|
+
this.playPauseButton.classList.add("button");
|
|
993
|
+
let isPlaying = false;
|
|
994
|
+
|
|
995
|
+
const setPlayingState = (playing: boolean) => {
|
|
996
|
+
isPlaying = playing;
|
|
997
|
+
if (playing) {
|
|
998
|
+
this.classList.add("is-playing");
|
|
999
|
+
} else {
|
|
1000
|
+
this.classList.remove("is-playing");
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
const togglePlayPause = () => {
|
|
1005
|
+
if (isPlaying) {
|
|
1006
|
+
if (this.player) {
|
|
1007
|
+
this.player.pause();
|
|
1008
|
+
}
|
|
1009
|
+
setPlayingState(false);
|
|
1010
|
+
} else {
|
|
1011
|
+
if (this.player) {
|
|
1012
|
+
this.player.play();
|
|
1013
|
+
}
|
|
1014
|
+
setPlayingState(true);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
this.setCustomControlState = setPlayingState;
|
|
1019
|
+
|
|
1020
|
+
buttonOverlay.addEventListener("click", togglePlayPause);
|
|
1021
|
+
if (this.playPauseButton) {
|
|
1022
|
+
buttonOverlay.appendChild(this.playPauseButton);
|
|
1023
|
+
}
|
|
1024
|
+
this.appendChild(buttonOverlay);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#updateBackgroundMode() {
|
|
1028
|
+
if (this.#background) {
|
|
1029
|
+
this.classList.add("is-background");
|
|
1030
|
+
this.#autoplay = true;
|
|
1031
|
+
this.#muted = true;
|
|
1032
|
+
this.#controls = false;
|
|
1033
|
+
this.#lazy = false;
|
|
1034
|
+
} else {
|
|
1035
|
+
this.classList.remove("is-background");
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
public async play(): Promise<void> {
|
|
1040
|
+
if (this.#lazy && !this.player && !this.iframe) {
|
|
1041
|
+
this.log(
|
|
1042
|
+
"VimeoEmbed: Lazy video needs to be loaded first. Initializing..."
|
|
1043
|
+
);
|
|
1044
|
+
this.setAttribute("data-should-autoplay", "true");
|
|
1045
|
+
await this.initializePlayer(this.#videoId);
|
|
1046
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (!this.playerReady) {
|
|
1050
|
+
this.warn("VimeoEmbed: Player is not ready. Waiting for initialization.");
|
|
1051
|
+
await new Promise((resolve) => {
|
|
1052
|
+
const interval = setInterval(() => {
|
|
1053
|
+
if (this.playerReady) {
|
|
1054
|
+
clearInterval(interval);
|
|
1055
|
+
resolve(null);
|
|
1056
|
+
}
|
|
1057
|
+
}, 100);
|
|
1058
|
+
setTimeout(() => {
|
|
1059
|
+
clearInterval(interval);
|
|
1060
|
+
resolve(null);
|
|
1061
|
+
}, 5000);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
this.removeAttribute("data-should-autoplay");
|
|
1066
|
+
|
|
1067
|
+
if (this.player) {
|
|
1068
|
+
this.player.play();
|
|
1069
|
+
this.log("VimeoEmbed: Video playback started via API.");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
this.#playing = true;
|
|
1073
|
+
if (this.setCustomControlState) {
|
|
1074
|
+
this.setCustomControlState(true);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
this.dispatchCustomEvent("play");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
public pause() {
|
|
1081
|
+
if (this.player) {
|
|
1082
|
+
this.player.pause();
|
|
1083
|
+
this.log("VimeoEmbed: Video paused via API.");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
this.#playing = false;
|
|
1087
|
+
if (this.setCustomControlState) {
|
|
1088
|
+
this.setCustomControlState(false);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
this.dispatchCustomEvent("pause");
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
public stopVideo() {
|
|
1095
|
+
if (this.player) {
|
|
1096
|
+
this.player.pause();
|
|
1097
|
+
this.player.setCurrentTime(0);
|
|
1098
|
+
this.log("VimeoEmbed: Video stopped via API.");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
this.#playing = false;
|
|
1102
|
+
if (this.setCustomControlState) {
|
|
1103
|
+
this.setCustomControlState(false);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
this.dispatchCustomEvent("stop");
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
public mute() {
|
|
1110
|
+
this.muted = true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
public unmute() {
|
|
1114
|
+
this.muted = false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
public togglePlay() {
|
|
1118
|
+
if (this.#playing) {
|
|
1119
|
+
this.pause();
|
|
1120
|
+
} else {
|
|
1121
|
+
this.play();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
public toggleMute() {
|
|
1126
|
+
this.muted = !this.#muted;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
public static toggleDebug(forceState?: boolean) {
|
|
1130
|
+
VimeoEmbed.DEBUG =
|
|
1131
|
+
forceState !== undefined ? forceState : !VimeoEmbed.DEBUG;
|
|
1132
|
+
console.log(
|
|
1133
|
+
`VimeoEmbed: Debugging is now ${
|
|
1134
|
+
VimeoEmbed.DEBUG ? "ENABLED" : "DISABLED"
|
|
1135
|
+
}.`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
public getPlayerState() {
|
|
1140
|
+
let actualMutedState = this.#muted;
|
|
1141
|
+
if (this.player && this.playerReady && !this.updatingMutedState) {
|
|
1142
|
+
try {
|
|
1143
|
+
this.player.getVolume().then((volume: number) => {
|
|
1144
|
+
actualMutedState = volume === 0;
|
|
1145
|
+
if (actualMutedState !== this.#muted && !this.updatingMutedState) {
|
|
1146
|
+
this.#muted = actualMutedState;
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
this.warn("VimeoEmbed: Could not get muted state from player:", error);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return {
|
|
1155
|
+
playing: this.#playing,
|
|
1156
|
+
muted: actualMutedState,
|
|
1157
|
+
videoId: this.#videoId,
|
|
1158
|
+
ready: this.playerReady,
|
|
1159
|
+
initialized: this.initialized,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
public isPlaying(): boolean {
|
|
1164
|
+
return this.#playing;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
public async isMuted(): Promise<boolean> {
|
|
1168
|
+
if (this.player && this.playerReady) {
|
|
1169
|
+
try {
|
|
1170
|
+
const volume = await this.player.getVolume();
|
|
1171
|
+
return volume === 0;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
this.warn("VimeoEmbed: Could not get muted state from player:", error);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return this.#muted;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
public async getCurrentTime(): Promise<number> {
|
|
1180
|
+
if (this.player && this.playerReady) {
|
|
1181
|
+
try {
|
|
1182
|
+
return await this.player.getCurrentTime();
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
this.warn("VimeoEmbed: Could not get current time from player:", error);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return 0;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Request fullscreen mode
|
|
1192
|
+
*/
|
|
1193
|
+
enterFullscreen(): Promise<void> {
|
|
1194
|
+
const elem = this as any;
|
|
1195
|
+
if (elem.requestFullscreen) {
|
|
1196
|
+
return elem.requestFullscreen();
|
|
1197
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
1198
|
+
return elem.webkitRequestFullscreen();
|
|
1199
|
+
} else if (elem.mozRequestFullScreen) {
|
|
1200
|
+
return elem.mozRequestFullScreen();
|
|
1201
|
+
} else if (elem.msRequestFullscreen) {
|
|
1202
|
+
return elem.msRequestFullscreen();
|
|
1203
|
+
}
|
|
1204
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Exit fullscreen mode
|
|
1209
|
+
*/
|
|
1210
|
+
exitFullscreen(): Promise<void> {
|
|
1211
|
+
const doc = document as any;
|
|
1212
|
+
if (doc.exitFullscreen) {
|
|
1213
|
+
return doc.exitFullscreen();
|
|
1214
|
+
} else if (doc.webkitExitFullscreen) {
|
|
1215
|
+
return doc.webkitExitFullscreen();
|
|
1216
|
+
} else if (doc.mozCancelFullScreen) {
|
|
1217
|
+
return doc.mozCancelFullScreen();
|
|
1218
|
+
} else if (doc.msExitFullscreen) {
|
|
1219
|
+
return doc.msExitFullscreen();
|
|
1220
|
+
}
|
|
1221
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Toggle fullscreen mode
|
|
1226
|
+
*/
|
|
1227
|
+
async toggleFullscreen(): Promise<void> {
|
|
1228
|
+
if (this.isFullscreen()) {
|
|
1229
|
+
await this.exitFullscreen();
|
|
1230
|
+
} else {
|
|
1231
|
+
await this.enterFullscreen();
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Check if currently in fullscreen mode
|
|
1237
|
+
*/
|
|
1238
|
+
isFullscreen(): boolean {
|
|
1239
|
+
const doc = document as any;
|
|
1240
|
+
return !!(
|
|
1241
|
+
doc.fullscreenElement ||
|
|
1242
|
+
doc.webkitFullscreenElement ||
|
|
1243
|
+
doc.mozFullScreenElement ||
|
|
1244
|
+
doc.msFullscreenElement
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Get available quality levels for the current video
|
|
1250
|
+
* @returns Array of available quality levels
|
|
1251
|
+
*/
|
|
1252
|
+
async getAvailableQualities(): Promise<string[]> {
|
|
1253
|
+
if (this.player && this.playerReady) {
|
|
1254
|
+
try {
|
|
1255
|
+
return (await this.player.getQualities()) || [];
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
this.warn("VimeoEmbed: Could not get available qualities:", error);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Get the current playback quality
|
|
1265
|
+
* @returns Current quality level
|
|
1266
|
+
*/
|
|
1267
|
+
async getCurrentQuality(): Promise<string> {
|
|
1268
|
+
if (this.player && this.playerReady) {
|
|
1269
|
+
try {
|
|
1270
|
+
return (await this.player.getQuality()) || "auto";
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
this.warn("VimeoEmbed: Could not get current quality:", error);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return "auto";
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Set the playback quality
|
|
1280
|
+
* @param quality The desired quality level
|
|
1281
|
+
*/
|
|
1282
|
+
async setQuality(quality: string): Promise<void> {
|
|
1283
|
+
if (!this.player || !this.playerReady) {
|
|
1284
|
+
this.warn("VimeoEmbed: Player not ready for quality change");
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
try {
|
|
1289
|
+
const oldQuality = await this.getCurrentQuality();
|
|
1290
|
+
await this.player.setQuality(quality);
|
|
1291
|
+
|
|
1292
|
+
// Dispatch quality change event
|
|
1293
|
+
const event = new CustomEvent("qualitychange", {
|
|
1294
|
+
detail: {
|
|
1295
|
+
oldQuality,
|
|
1296
|
+
newQuality: quality,
|
|
1297
|
+
availableQualities: await this.getAvailableQualities(),
|
|
1298
|
+
},
|
|
1299
|
+
bubbles: true,
|
|
1300
|
+
composed: true,
|
|
1301
|
+
});
|
|
1302
|
+
this.dispatchEvent(event);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
this.warn("VimeoEmbed: Could not set quality:", error);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
public loadVideo(videoIdOrUrl: string) {
|
|
1309
|
+
const extracted = this.extractVideoId(videoIdOrUrl);
|
|
1310
|
+
if (extracted) {
|
|
1311
|
+
this.#url = videoIdOrUrl;
|
|
1312
|
+
this.#videoId = extracted;
|
|
1313
|
+
} else {
|
|
1314
|
+
this.#videoId = videoIdOrUrl;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (this.#lazy && !this.player) {
|
|
1318
|
+
try {
|
|
1319
|
+
this.initializePlayer(this.#videoId);
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
this.warn("VimeoEmbed: loadVideo failed to initialize player:", error);
|
|
1322
|
+
}
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (this.initialized) {
|
|
1327
|
+
this.reinitializePlayer();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
customElements.define("vimeo-embed", VimeoEmbed);
|
|
1333
|
+
|
|
1334
|
+
declare global {
|
|
1335
|
+
interface Window {
|
|
1336
|
+
Vimeo?: any;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
export {};
|