@smartimpact-it/modern-video-embed 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +322 -71
- package/dist/components/BaseVideoEmbed.d.ts +91 -0
- package/dist/components/BaseVideoEmbed.d.ts.map +1 -0
- package/dist/components/BaseVideoEmbed.js +275 -0
- package/dist/components/BaseVideoEmbed.js.map +1 -0
- package/dist/components/VideoEmbed.d.ts +68 -0
- package/dist/components/VideoEmbed.d.ts.map +1 -0
- package/dist/components/VideoEmbed.js +786 -0
- package/dist/components/VideoEmbed.js.map +1 -0
- package/dist/components/VimeoEmbed.d.ts +26 -36
- package/dist/components/VimeoEmbed.d.ts.map +1 -1
- package/dist/components/VimeoEmbed.js +231 -326
- package/dist/components/VimeoEmbed.js.map +1 -1
- package/dist/components/VimeoEmbed.min.js +1 -1
- package/dist/components/YouTubeEmbed.d.ts +108 -42
- package/dist/components/YouTubeEmbed.d.ts.map +1 -1
- package/dist/components/YouTubeEmbed.js +361 -375
- package/dist/components/YouTubeEmbed.js.map +1 -1
- package/dist/components/YouTubeEmbed.min.js +1 -1
- package/dist/css/components.css +285 -68
- package/dist/css/components.css.map +1 -1
- package/dist/css/components.min.css +1 -1
- package/dist/css/main.css +285 -68
- package/dist/css/main.css.map +1 -1
- package/dist/css/main.min.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/video-only.d.ts +7 -0
- package/dist/video-only.d.ts.map +1 -0
- package/dist/video-only.js +8 -0
- package/dist/video-only.js.map +1 -0
- package/dist/vimeo-only.d.ts +2 -2
- package/dist/vimeo-only.d.ts.map +1 -1
- package/dist/vimeo-only.js +2 -2
- package/dist/vimeo-only.js.map +1 -1
- package/dist/vimeo-only.min.js +1 -1
- package/dist/youtube-only.d.ts +2 -2
- package/dist/youtube-only.d.ts.map +1 -1
- package/dist/youtube-only.js +2 -2
- package/dist/youtube-only.js.map +1 -1
- package/dist/youtube-only.min.js +1 -1
- package/package.json +6 -5
- package/src/components/BaseVideoEmbed.ts +335 -0
- package/src/components/VideoEmbed.ts +870 -0
- package/src/components/VideoEmbed.ts.backup +1051 -0
- package/src/components/VimeoEmbed.ts +258 -395
- package/src/components/YouTubeEmbed.ts +378 -432
- package/src/index.ts +1 -0
- package/src/styles/_embed-base.scss +275 -0
- package/src/styles/_shared-functions.scss +56 -0
- package/src/styles/components.scss +4 -3
- package/src/styles/main.scss +7 -5
- package/src/styles/video-embed.scss +55 -0
- package/src/styles/vimeo-embed.scss +8 -248
- package/src/styles/youtube-embed.scss +8 -254
- package/src/types/index.ts +1 -0
- package/src/types/video-embed.d.ts +90 -0
- package/src/video-only.ts +9 -0
- package/src/vimeo-only.ts +2 -2
- package/src/youtube-only.ts +2 -2
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import { BaseVideoEmbed } from "./BaseVideoEmbed.js";
|
|
2
|
+
|
|
3
|
+
export class VideoEmbed extends BaseVideoEmbed {
|
|
4
|
+
private video: HTMLVideoElement | null = null;
|
|
5
|
+
|
|
6
|
+
// Video-specific properties
|
|
7
|
+
#url: string = "";
|
|
8
|
+
#loop: boolean = false;
|
|
9
|
+
#preload: "" | "none" | "metadata" | "auto" = "metadata";
|
|
10
|
+
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return [
|
|
13
|
+
"url",
|
|
14
|
+
"autoplay",
|
|
15
|
+
"controls",
|
|
16
|
+
"lazy",
|
|
17
|
+
"muted",
|
|
18
|
+
"poster",
|
|
19
|
+
"background",
|
|
20
|
+
"loop",
|
|
21
|
+
"preload",
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected getComponentName(): string {
|
|
26
|
+
return "VideoEmbed";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attributeChangedCallback(
|
|
30
|
+
name: string,
|
|
31
|
+
oldValue: string | null,
|
|
32
|
+
newValue: string | null,
|
|
33
|
+
) {
|
|
34
|
+
if (this.updatingAttribute) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.log(`Attribute changed - ${name}: ${oldValue} -> ${newValue}`);
|
|
39
|
+
|
|
40
|
+
switch (name) {
|
|
41
|
+
case "url":
|
|
42
|
+
this.#url = newValue || "";
|
|
43
|
+
if (this.initialized) {
|
|
44
|
+
this.reinitializePlayer();
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "autoplay":
|
|
48
|
+
this._autoplay = newValue !== null;
|
|
49
|
+
if (
|
|
50
|
+
!this._playing &&
|
|
51
|
+
this._autoplay &&
|
|
52
|
+
!this._lazy &&
|
|
53
|
+
this.initialized
|
|
54
|
+
) {
|
|
55
|
+
this.play();
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case "controls":
|
|
59
|
+
this._controls = newValue !== null;
|
|
60
|
+
if (this.video) {
|
|
61
|
+
this.video.controls = this._controls;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case "lazy":
|
|
65
|
+
this._lazy = newValue !== null;
|
|
66
|
+
break;
|
|
67
|
+
case "muted":
|
|
68
|
+
this._muted = newValue !== null;
|
|
69
|
+
if (this.video) {
|
|
70
|
+
this.video.muted = this._muted;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "poster":
|
|
74
|
+
this._poster = newValue || "";
|
|
75
|
+
if (this._lazy && !this.video) {
|
|
76
|
+
this.showPoster();
|
|
77
|
+
} else if (this.video) {
|
|
78
|
+
this.video.poster = this._poster;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "background":
|
|
82
|
+
this._background = newValue !== null;
|
|
83
|
+
this.updateBackgroundMode();
|
|
84
|
+
break;
|
|
85
|
+
case "loop":
|
|
86
|
+
this.#loop = newValue !== null;
|
|
87
|
+
if (this.video) {
|
|
88
|
+
this.video.loop = this.#loop;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "preload":
|
|
92
|
+
const validPreloadValues: Array<"" | "none" | "metadata" | "auto"> = [
|
|
93
|
+
"none",
|
|
94
|
+
"metadata",
|
|
95
|
+
"auto",
|
|
96
|
+
"",
|
|
97
|
+
];
|
|
98
|
+
this.#preload =
|
|
99
|
+
validPreloadValues.indexOf(newValue as any) !== -1
|
|
100
|
+
? (newValue as "" | "none" | "metadata" | "auto")
|
|
101
|
+
: "metadata";
|
|
102
|
+
if (this.video) {
|
|
103
|
+
this.video.preload = this.#preload;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Getters and setters
|
|
110
|
+
get url() {
|
|
111
|
+
return this.#url;
|
|
112
|
+
}
|
|
113
|
+
set url(value: string) {
|
|
114
|
+
this.#url = value;
|
|
115
|
+
this.reflectAttribute("url", value);
|
|
116
|
+
if (this.initialized) {
|
|
117
|
+
this.reinitializePlayer();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get autoplay() {
|
|
122
|
+
return this._autoplay;
|
|
123
|
+
}
|
|
124
|
+
set autoplay(value: boolean) {
|
|
125
|
+
this._autoplay = value;
|
|
126
|
+
this.reflectBooleanAttribute("autoplay", value);
|
|
127
|
+
if (!this._playing && value && !this._lazy && this.initialized) {
|
|
128
|
+
this.play();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get controls() {
|
|
133
|
+
return this._controls;
|
|
134
|
+
}
|
|
135
|
+
set controls(value: boolean) {
|
|
136
|
+
// Background mode videos must not have controls
|
|
137
|
+
if (this._background && value) {
|
|
138
|
+
this.warn("Cannot enable controls on background video");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this._controls = value;
|
|
142
|
+
this.reflectBooleanAttribute("controls", value);
|
|
143
|
+
if (this.video) {
|
|
144
|
+
this.video.controls = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get lazy() {
|
|
149
|
+
return this._lazy;
|
|
150
|
+
}
|
|
151
|
+
set lazy(value: boolean) {
|
|
152
|
+
this._lazy = value;
|
|
153
|
+
this.reflectBooleanAttribute("lazy", value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get muted() {
|
|
157
|
+
return this._muted;
|
|
158
|
+
}
|
|
159
|
+
set muted(value: boolean) {
|
|
160
|
+
// Background mode videos must be muted
|
|
161
|
+
if (this._background && !value) {
|
|
162
|
+
this.warn("Cannot unmute background video");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this._muted = value;
|
|
166
|
+
this.reflectBooleanAttribute("muted", value);
|
|
167
|
+
if (this.video) {
|
|
168
|
+
this.video.muted = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get poster() {
|
|
173
|
+
return this._poster;
|
|
174
|
+
}
|
|
175
|
+
set poster(value: string) {
|
|
176
|
+
this._poster = value;
|
|
177
|
+
this.reflectAttribute("poster", value);
|
|
178
|
+
if (this._lazy && !this.video) {
|
|
179
|
+
this.showPoster();
|
|
180
|
+
} else if (this.video) {
|
|
181
|
+
this.video.poster = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get playing() {
|
|
186
|
+
return this._playing;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get background() {
|
|
190
|
+
return this._background;
|
|
191
|
+
}
|
|
192
|
+
set background(value: boolean) {
|
|
193
|
+
this._background = value;
|
|
194
|
+
this.reflectBooleanAttribute("background", value);
|
|
195
|
+
this.updateBackgroundMode();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get loop() {
|
|
199
|
+
return this.#loop;
|
|
200
|
+
}
|
|
201
|
+
set loop(value: boolean) {
|
|
202
|
+
this.#loop = value;
|
|
203
|
+
this.reflectBooleanAttribute("loop", value);
|
|
204
|
+
if (this.video) {
|
|
205
|
+
this.video.loop = value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get preload() {
|
|
210
|
+
return this.#preload;
|
|
211
|
+
}
|
|
212
|
+
set preload(value: "" | "none" | "metadata" | "auto") {
|
|
213
|
+
this.#preload = value;
|
|
214
|
+
this.reflectAttribute("preload", value);
|
|
215
|
+
if (this.video) {
|
|
216
|
+
this.video.preload = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Implement abstract methods
|
|
221
|
+
protected handlePlay(): void {
|
|
222
|
+
if (this.video) {
|
|
223
|
+
this.video.play().catch((error) => {
|
|
224
|
+
this.warn("Play failed:", error);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected handlePause(): void {
|
|
230
|
+
if (this.video) {
|
|
231
|
+
this.video.pause();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected override handleRetry(): void {
|
|
236
|
+
if (this.#url) {
|
|
237
|
+
this.initializePlayer();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private setupFullscreenListener(): void {
|
|
242
|
+
const handleFullscreenChange = (event: Event) => {
|
|
243
|
+
if (!event.isTrusted) return;
|
|
244
|
+
|
|
245
|
+
const doc = document as any;
|
|
246
|
+
const fullscreenElement =
|
|
247
|
+
doc.fullscreenElement ||
|
|
248
|
+
doc.webkitFullscreenElement ||
|
|
249
|
+
doc.mozFullScreenElement ||
|
|
250
|
+
doc.msFullscreenElement;
|
|
251
|
+
|
|
252
|
+
const isFullscreen =
|
|
253
|
+
fullscreenElement === this || fullscreenElement === this.video;
|
|
254
|
+
const wasFullscreen = this.classList.contains("is-fullscreen");
|
|
255
|
+
|
|
256
|
+
if (isFullscreen !== wasFullscreen) {
|
|
257
|
+
this.dispatchEvent(
|
|
258
|
+
new CustomEvent("fullscreenchange", {
|
|
259
|
+
detail: { isFullscreen },
|
|
260
|
+
bubbles: false,
|
|
261
|
+
composed: false,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (isFullscreen) {
|
|
266
|
+
this.classList.add("is-fullscreen");
|
|
267
|
+
} else {
|
|
268
|
+
this.classList.remove("is-fullscreen");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
274
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
275
|
+
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
276
|
+
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private setupKeyboardHandlers(): void {
|
|
280
|
+
this.keyboardHandler = (e: KeyboardEvent) => {
|
|
281
|
+
if (!this.playerReady || (e.target as HTMLElement)?.tagName === "INPUT") {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
switch (e.key.toLowerCase()) {
|
|
286
|
+
case "k":
|
|
287
|
+
case " ":
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
this.togglePlay();
|
|
290
|
+
this.announceToScreenReader(
|
|
291
|
+
this._playing ? "Video playing" : "Video paused",
|
|
292
|
+
);
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case "m":
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
this.toggleMute();
|
|
298
|
+
this.announceToScreenReader(
|
|
299
|
+
this._muted ? "Video muted" : "Video unmuted",
|
|
300
|
+
);
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
case "f":
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
if (document.fullscreenElement) {
|
|
306
|
+
document.exitFullscreen().catch((error) => {
|
|
307
|
+
this.warn("Exit fullscreen failed:", error);
|
|
308
|
+
});
|
|
309
|
+
this.announceToScreenReader("Exited fullscreen");
|
|
310
|
+
} else {
|
|
311
|
+
this.enterFullscreen()
|
|
312
|
+
.then(() => {
|
|
313
|
+
this.announceToScreenReader("Entered fullscreen");
|
|
314
|
+
})
|
|
315
|
+
.catch((error) => {
|
|
316
|
+
this.warn("Enter fullscreen failed:", error);
|
|
317
|
+
this.announceToScreenReader("Fullscreen not available");
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
this.addEventListener("keydown", this.keyboardHandler as EventListener);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private showPoster() {
|
|
328
|
+
const posterUrl = this._poster || "";
|
|
329
|
+
this.log("Using poster URL:", posterUrl);
|
|
330
|
+
|
|
331
|
+
this.innerHTML = "";
|
|
332
|
+
|
|
333
|
+
// Create poster image if URL is provided
|
|
334
|
+
if (posterUrl) {
|
|
335
|
+
const poster = document.createElement("img");
|
|
336
|
+
poster.src = posterUrl;
|
|
337
|
+
poster.alt = "Video thumbnail";
|
|
338
|
+
poster.classList.add("video-poster");
|
|
339
|
+
poster.loading = "lazy";
|
|
340
|
+
|
|
341
|
+
// Add error handling for poster loading
|
|
342
|
+
poster.onerror = () => {
|
|
343
|
+
this.warn("VideoEmbed: Poster failed to load.");
|
|
344
|
+
poster.style.display = "none";
|
|
345
|
+
this.style.backgroundColor = "#000000";
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
this.appendChild(poster);
|
|
349
|
+
} else {
|
|
350
|
+
// Create a placeholder div if no poster URL
|
|
351
|
+
const poster = document.createElement("div");
|
|
352
|
+
poster.classList.add("video-poster");
|
|
353
|
+
poster.setAttribute("role", "img");
|
|
354
|
+
poster.setAttribute("aria-label", "Video thumbnail");
|
|
355
|
+
poster.style.backgroundColor = "#000000";
|
|
356
|
+
this.appendChild(poster);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const buttonOverlay = document.createElement("div");
|
|
360
|
+
buttonOverlay.classList.add("button-overlay");
|
|
361
|
+
buttonOverlay.setAttribute("role", "button");
|
|
362
|
+
buttonOverlay.setAttribute("tabindex", "0");
|
|
363
|
+
buttonOverlay.setAttribute("aria-label", "Play video");
|
|
364
|
+
|
|
365
|
+
const button = document.createElement("div");
|
|
366
|
+
button.classList.add("button");
|
|
367
|
+
button.setAttribute("aria-hidden", "true");
|
|
368
|
+
|
|
369
|
+
buttonOverlay.appendChild(button);
|
|
370
|
+
|
|
371
|
+
const loadVideo: EventListener = () => {
|
|
372
|
+
this.log("Loading video from poster click");
|
|
373
|
+
this.setAttribute("data-poster-autoplay", "true");
|
|
374
|
+
try {
|
|
375
|
+
this.initializePlayer();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
this.error("Error initializing player from poster:", error);
|
|
378
|
+
this.dispatchCustomEvent("error", { message: "Failed to load video" });
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
this.posterClickHandler = loadVideo;
|
|
383
|
+
|
|
384
|
+
const keyboardActivate = (e: KeyboardEvent) => {
|
|
385
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
loadVideo(e);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const posterElement = this.querySelector(".video-poster");
|
|
392
|
+
if (posterElement) {
|
|
393
|
+
posterElement.addEventListener("click", loadVideo);
|
|
394
|
+
}
|
|
395
|
+
buttonOverlay.addEventListener("click", loadVideo);
|
|
396
|
+
buttonOverlay.addEventListener(
|
|
397
|
+
"keydown",
|
|
398
|
+
keyboardActivate as EventListener,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
this.appendChild(buttonOverlay);
|
|
402
|
+
|
|
403
|
+
this.log("Poster displayed for lazy loading");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
protected async initializePlayer(): Promise<void> {
|
|
407
|
+
this.log("Initializing player for URL:", this.#url);
|
|
408
|
+
|
|
409
|
+
const isBackground = this._background;
|
|
410
|
+
const autoplay =
|
|
411
|
+
isBackground ||
|
|
412
|
+
this.hasAttribute("autoplay") ||
|
|
413
|
+
this.hasAttribute("data-poster-autoplay") ||
|
|
414
|
+
this.hasAttribute("data-should-autoplay");
|
|
415
|
+
const controls = isBackground ? false : this.hasAttribute("controls");
|
|
416
|
+
const mute = isBackground || autoplay;
|
|
417
|
+
|
|
418
|
+
if (this.hasAttribute("data-poster-autoplay")) {
|
|
419
|
+
this.removeAttribute("data-poster-autoplay");
|
|
420
|
+
}
|
|
421
|
+
if (this.hasAttribute("data-should-autoplay")) {
|
|
422
|
+
this.removeAttribute("data-should-autoplay");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (autoplay && !this._muted) {
|
|
426
|
+
this._muted = true;
|
|
427
|
+
this.log("Autoplay enabled, forcing muted state for compliance");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.innerHTML = "";
|
|
431
|
+
|
|
432
|
+
this.video = document.createElement("video");
|
|
433
|
+
this.video.style.objectFit = this._background ? "cover" : "contain";
|
|
434
|
+
|
|
435
|
+
this.video.src = this.#url;
|
|
436
|
+
this.video.controls = controls;
|
|
437
|
+
this.video.autoplay = autoplay;
|
|
438
|
+
this.video.muted = mute || this._muted;
|
|
439
|
+
this.video.loop = this.#loop;
|
|
440
|
+
this.video.preload = this.#preload;
|
|
441
|
+
this.video.playsInline = true;
|
|
442
|
+
|
|
443
|
+
if (this._poster) {
|
|
444
|
+
this.video.poster = this._poster;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
this.setupVideoEvents();
|
|
448
|
+
this.appendChild(this.video);
|
|
449
|
+
|
|
450
|
+
if (!controls) {
|
|
451
|
+
this.addCustomControls("video-embed-container");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.playerReady = true;
|
|
455
|
+
this.dispatchCustomEvent("ready", {
|
|
456
|
+
url: this.#url,
|
|
457
|
+
muted: this._muted,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (autoplay) {
|
|
461
|
+
this.video.play().catch((error) => {
|
|
462
|
+
this.warn("Autoplay failed:", error);
|
|
463
|
+
this.dispatchCustomEvent("error", {
|
|
464
|
+
message: "Autoplay failed",
|
|
465
|
+
error,
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
protected reinitializePlayer(): void {
|
|
472
|
+
if (!this.initialized || !this.#url) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.video) {
|
|
477
|
+
this.video.pause();
|
|
478
|
+
this.video.remove();
|
|
479
|
+
this.video = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this.playerReady = false;
|
|
483
|
+
this._playing = false;
|
|
484
|
+
this.classList.remove("is-playing");
|
|
485
|
+
|
|
486
|
+
if (this._lazy) {
|
|
487
|
+
this.showPoster();
|
|
488
|
+
} else {
|
|
489
|
+
this.initializePlayer();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
protected destroyPlayer(): void {
|
|
494
|
+
if (this.video) {
|
|
495
|
+
this.video.pause();
|
|
496
|
+
this.video.src = "";
|
|
497
|
+
this.video.load();
|
|
498
|
+
this.video = null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private setupVideoEvents() {
|
|
503
|
+
if (!this.video) return;
|
|
504
|
+
|
|
505
|
+
this.video.addEventListener("play", () => {
|
|
506
|
+
this._playing = true;
|
|
507
|
+
this.classList.add("is-playing");
|
|
508
|
+
if (this.setCustomControlState) {
|
|
509
|
+
this.setCustomControlState(true);
|
|
510
|
+
}
|
|
511
|
+
this.dispatchCustomEvent("play", {
|
|
512
|
+
url: this.#url,
|
|
513
|
+
currentTime: this.video?.currentTime || 0,
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
this.video.addEventListener("pause", () => {
|
|
518
|
+
this._playing = false;
|
|
519
|
+
this.classList.remove("is-playing");
|
|
520
|
+
if (this.setCustomControlState) {
|
|
521
|
+
this.setCustomControlState(false);
|
|
522
|
+
}
|
|
523
|
+
this.dispatchCustomEvent("pause", {
|
|
524
|
+
url: this.#url,
|
|
525
|
+
currentTime: this.video?.currentTime || 0,
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
this.video.addEventListener("ended", () => {
|
|
530
|
+
this._playing = false;
|
|
531
|
+
this.classList.remove("is-playing");
|
|
532
|
+
if (this.setCustomControlState) {
|
|
533
|
+
this.setCustomControlState(false);
|
|
534
|
+
}
|
|
535
|
+
this.dispatchCustomEvent("ended", {
|
|
536
|
+
url: this.#url,
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
this.video.addEventListener("timeupdate", () => {
|
|
541
|
+
this.dispatchCustomEvent("timeupdate", {
|
|
542
|
+
url: this.#url,
|
|
543
|
+
currentTime: this.video?.currentTime || 0,
|
|
544
|
+
duration: this.video?.duration || 0,
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
this.video.addEventListener("volumechange", () => {
|
|
549
|
+
this._muted = this.video?.muted || false;
|
|
550
|
+
this.dispatchCustomEvent("volumechange", {
|
|
551
|
+
muted: this._muted,
|
|
552
|
+
volume: this.video?.volume || 0,
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
this.video.addEventListener("error", (e) => {
|
|
557
|
+
this.error("Video error:", e);
|
|
558
|
+
this.dispatchCustomEvent("error", {
|
|
559
|
+
message: "Video playback error",
|
|
560
|
+
error: e,
|
|
561
|
+
});
|
|
562
|
+
this.showErrorMessage(
|
|
563
|
+
"Failed to load video. Please check the URL or try again later.",
|
|
564
|
+
"video-error-message",
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
this.video.addEventListener("loadedmetadata", () => {
|
|
569
|
+
const width = this.video?.videoWidth || 0;
|
|
570
|
+
const height = this.video?.videoHeight || 0;
|
|
571
|
+
|
|
572
|
+
// Automatically set aspect ratio CSS custom properties
|
|
573
|
+
if (width > 0 && height > 0) {
|
|
574
|
+
this.setAspectRatio(width, height);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.dispatchCustomEvent("loadedmetadata", {
|
|
578
|
+
duration: this.video?.duration || 0,
|
|
579
|
+
videoWidth: width,
|
|
580
|
+
videoHeight: height,
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
override connectedCallback() {
|
|
586
|
+
super.connectedCallback();
|
|
587
|
+
|
|
588
|
+
const urlAttr = this.getAttribute("url");
|
|
589
|
+
if (urlAttr) {
|
|
590
|
+
this.#url = urlAttr;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this._autoplay = this.hasAttribute("autoplay");
|
|
594
|
+
this._controls = this.hasAttribute("controls");
|
|
595
|
+
this._lazy = this.hasAttribute("lazy");
|
|
596
|
+
this._muted = this.hasAttribute("muted");
|
|
597
|
+
this._background = this.hasAttribute("background");
|
|
598
|
+
this.#loop = this.hasAttribute("loop");
|
|
599
|
+
this._poster = this.getAttribute("poster") || "";
|
|
600
|
+
const preloadAttr = this.getAttribute("preload");
|
|
601
|
+
this.#preload =
|
|
602
|
+
preloadAttr === "none" ||
|
|
603
|
+
preloadAttr === "metadata" ||
|
|
604
|
+
preloadAttr === "auto"
|
|
605
|
+
? preloadAttr
|
|
606
|
+
: "metadata";
|
|
607
|
+
|
|
608
|
+
if (!this.#url) {
|
|
609
|
+
this.error("Missing video URL");
|
|
610
|
+
this.dispatchCustomEvent("error", {
|
|
611
|
+
message: "Missing video URL",
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
this.classList.add("video-embed-wrapper");
|
|
617
|
+
|
|
618
|
+
this.setAttribute("role", "region");
|
|
619
|
+
this.setAttribute("aria-label", "Video player");
|
|
620
|
+
if (!this.hasAttribute("tabindex")) {
|
|
621
|
+
this.setAttribute("tabindex", "0");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this.ariaLiveRegion = document.createElement("div");
|
|
625
|
+
this.ariaLiveRegion.setAttribute("aria-live", "polite");
|
|
626
|
+
this.ariaLiveRegion.setAttribute("aria-atomic", "true");
|
|
627
|
+
this.ariaLiveRegion.className = "sr-only";
|
|
628
|
+
this.ariaLiveRegion.style.cssText =
|
|
629
|
+
"position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;";
|
|
630
|
+
this.appendChild(this.ariaLiveRegion);
|
|
631
|
+
|
|
632
|
+
this.setupKeyboardHandlers();
|
|
633
|
+
this.setupFullscreenListener();
|
|
634
|
+
|
|
635
|
+
this.updateBackgroundMode();
|
|
636
|
+
|
|
637
|
+
if (this._lazy) {
|
|
638
|
+
this.showPoster();
|
|
639
|
+
this.setupLazyLoading(() => {
|
|
640
|
+
const shouldAutoplay =
|
|
641
|
+
this.getAttribute("data-poster-autoplay") === "true";
|
|
642
|
+
if (!shouldAutoplay) {
|
|
643
|
+
this.initializePlayer();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
} else {
|
|
647
|
+
this.initializePlayer();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.initialized = true;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
override disconnectedCallback() {
|
|
654
|
+
super.disconnectedCallback();
|
|
655
|
+
|
|
656
|
+
const poster = this.querySelector(".video-poster");
|
|
657
|
+
const buttonOverlay = this.querySelector(".button-overlay");
|
|
658
|
+
|
|
659
|
+
if (poster && this.posterClickHandler) {
|
|
660
|
+
poster.removeEventListener("click", this.posterClickHandler);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (buttonOverlay && this.posterClickHandler) {
|
|
664
|
+
buttonOverlay.removeEventListener("click", this.posterClickHandler);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
this.innerHTML = "";
|
|
668
|
+
this.classList.remove(
|
|
669
|
+
"video-embed-wrapper",
|
|
670
|
+
"video-embed-container",
|
|
671
|
+
"is-playing",
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
this.log("Cleanup complete.");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Public API methods
|
|
678
|
+
public async play(): Promise<void> {
|
|
679
|
+
if (this._lazy && !this.video) {
|
|
680
|
+
this.log("Lazy video needs to be loaded first. Initializing...");
|
|
681
|
+
this.setAttribute("data-should-autoplay", "true");
|
|
682
|
+
await this.initializePlayer();
|
|
683
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this.removeAttribute("data-should-autoplay");
|
|
687
|
+
|
|
688
|
+
if (this.video) {
|
|
689
|
+
try {
|
|
690
|
+
await this.video.play();
|
|
691
|
+
this.log("Video playback started.");
|
|
692
|
+
} catch (error) {
|
|
693
|
+
this.warn("Play failed:", error);
|
|
694
|
+
this.dispatchCustomEvent("error", {
|
|
695
|
+
message: "Failed to play video",
|
|
696
|
+
error,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
public pause(): void {
|
|
703
|
+
if (this.video) {
|
|
704
|
+
this.video.pause();
|
|
705
|
+
this.log("Video paused.");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
public stopVideo(): void {
|
|
710
|
+
if (this.video) {
|
|
711
|
+
this.video.pause();
|
|
712
|
+
this.video.currentTime = 0;
|
|
713
|
+
this.log("Video stopped.");
|
|
714
|
+
this.dispatchCustomEvent("stop", {
|
|
715
|
+
url: this.#url,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
public mute(): void {
|
|
721
|
+
this._muted = true;
|
|
722
|
+
this.reflectBooleanAttribute("muted", true);
|
|
723
|
+
if (this.video) {
|
|
724
|
+
this.video.muted = true;
|
|
725
|
+
}
|
|
726
|
+
this.log("Video muted.");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
public unmute(): void {
|
|
730
|
+
this._muted = false;
|
|
731
|
+
this.reflectBooleanAttribute("muted", false);
|
|
732
|
+
if (this.video) {
|
|
733
|
+
this.video.muted = false;
|
|
734
|
+
}
|
|
735
|
+
this.log("Video unmuted.");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
public togglePlay(): void {
|
|
739
|
+
if (this._playing) {
|
|
740
|
+
this.pause();
|
|
741
|
+
} else {
|
|
742
|
+
this.play();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
public toggleMute(): void {
|
|
747
|
+
if (this._muted) {
|
|
748
|
+
this.unmute();
|
|
749
|
+
} else {
|
|
750
|
+
this.mute();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
public loadVideo(url?: string): void {
|
|
755
|
+
if (url) {
|
|
756
|
+
this.#url = url;
|
|
757
|
+
this.reflectAttribute("url", url);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (this._lazy && !this.video) {
|
|
761
|
+
this.initializePlayer();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (this.initialized) {
|
|
766
|
+
this.reinitializePlayer();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
public getPlayerState() {
|
|
771
|
+
return {
|
|
772
|
+
playing: this._playing,
|
|
773
|
+
muted: this._muted,
|
|
774
|
+
url: this.#url,
|
|
775
|
+
ready: this.playerReady,
|
|
776
|
+
initialized: this.initialized,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
public getCurrentTime(): number {
|
|
781
|
+
return this.video?.currentTime || 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
public getDuration(): number {
|
|
785
|
+
return this.video?.duration || 0;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
public setCurrentTime(time: number): void {
|
|
789
|
+
if (this.video) {
|
|
790
|
+
this.video.currentTime = time;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
public setVolume(volume: number): void {
|
|
795
|
+
if (this.video) {
|
|
796
|
+
this.video.volume = Math.max(0, Math.min(1, volume));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
public getVolume(): number {
|
|
801
|
+
return this.video?.volume || 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
public isPlaying(): boolean {
|
|
805
|
+
return this._playing;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
public isMuted(): boolean {
|
|
809
|
+
return this._muted;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
public enterFullscreen(): Promise<void> {
|
|
813
|
+
const elem = (this.video || this) as any;
|
|
814
|
+
if (elem.requestFullscreen) {
|
|
815
|
+
return elem.requestFullscreen();
|
|
816
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
817
|
+
return elem.webkitRequestFullscreen();
|
|
818
|
+
} else if (elem.mozRequestFullScreen) {
|
|
819
|
+
return elem.mozRequestFullScreen();
|
|
820
|
+
} else if (elem.msRequestFullscreen) {
|
|
821
|
+
return elem.msRequestFullscreen();
|
|
822
|
+
}
|
|
823
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
public exitFullscreen(): Promise<void> {
|
|
827
|
+
const doc = document as any;
|
|
828
|
+
if (doc.exitFullscreen) {
|
|
829
|
+
return doc.exitFullscreen();
|
|
830
|
+
} else if (doc.webkitExitFullscreen) {
|
|
831
|
+
return doc.webkitExitFullscreen();
|
|
832
|
+
} else if (doc.mozCancelFullScreen) {
|
|
833
|
+
return doc.mozCancelFullScreen();
|
|
834
|
+
} else if (doc.msExitFullscreen) {
|
|
835
|
+
return doc.msExitFullscreen();
|
|
836
|
+
}
|
|
837
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
public async toggleFullscreen(): Promise<void> {
|
|
841
|
+
try {
|
|
842
|
+
if (this.isFullscreen()) {
|
|
843
|
+
await this.exitFullscreen();
|
|
844
|
+
} else {
|
|
845
|
+
await this.enterFullscreen();
|
|
846
|
+
}
|
|
847
|
+
} catch (error) {
|
|
848
|
+
this.warn("Toggle fullscreen failed:", error);
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
public isFullscreen(): boolean {
|
|
854
|
+
const doc = document as any;
|
|
855
|
+
const fullscreenElement =
|
|
856
|
+
doc.fullscreenElement ||
|
|
857
|
+
doc.webkitFullscreenElement ||
|
|
858
|
+
doc.mozFullScreenElement ||
|
|
859
|
+
doc.msFullscreenElement;
|
|
860
|
+
return fullscreenElement === this || fullscreenElement === this.video;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
public static override toggleDebug(forceState?: boolean): void {
|
|
864
|
+
BaseVideoEmbed.toggleDebug(forceState);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
customElements.define("video-embed", VideoEmbed);
|
|
869
|
+
|
|
870
|
+
export {};
|