@smartimpact-it/modern-video-embed 2.0.4 → 2.0.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 +322 -71
- package/dist/components/BaseVideoEmbed.d.ts +86 -0
- package/dist/components/BaseVideoEmbed.d.ts.map +1 -0
- package/dist/components/BaseVideoEmbed.js +256 -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 +770 -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 +205 -328
- 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 +341 -373
- package/dist/components/YouTubeEmbed.js.map +1 -1
- package/dist/components/YouTubeEmbed.min.js +1 -1
- package/dist/css/components.css +235 -44
- package/dist/css/components.css.map +1 -1
- package/dist/css/components.min.css +1 -1
- package/dist/css/main.css +235 -44
- 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 +303 -0
- package/src/components/VideoEmbed.ts +852 -0
- package/src/components/VideoEmbed.ts.backup +1051 -0
- package/src/components/VimeoEmbed.ts +233 -397
- package/src/components/YouTubeEmbed.ts +359 -430
- package/src/index.ts +1 -0
- package/src/styles/_embed-base.scss +255 -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 +37 -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,1051 @@
|
|
|
1
|
+
export class VideoEmbed extends HTMLElement {
|
|
2
|
+
private video: HTMLVideoElement | null = null;
|
|
3
|
+
private static instanceCount = 0;
|
|
4
|
+
private static DEBUG = false;
|
|
5
|
+
private playerReady = false;
|
|
6
|
+
private playPauseButton: HTMLDivElement | null = null;
|
|
7
|
+
private initialized = false;
|
|
8
|
+
private setCustomControlState: ((playing: boolean) => void) | null = null;
|
|
9
|
+
private updatingAttribute = false;
|
|
10
|
+
|
|
11
|
+
/** The full video URL (e.g., "https://example.com/video.mp4") */
|
|
12
|
+
#url: string = "";
|
|
13
|
+
/** Whether the video should autoplay when loaded. Requires `muted` to be true. */
|
|
14
|
+
#autoplay: boolean = false;
|
|
15
|
+
/** Whether the video audio is muted. */
|
|
16
|
+
#muted: boolean = false;
|
|
17
|
+
/** Whether to show the native video player controls. */
|
|
18
|
+
#controls: boolean = false;
|
|
19
|
+
/** Whether to lazy-load the video, showing a poster image until interaction. */
|
|
20
|
+
#lazy: boolean = false;
|
|
21
|
+
/** URL for a custom poster image. */
|
|
22
|
+
#poster: string = "";
|
|
23
|
+
/** Read-only property to check if the video is currently playing. */
|
|
24
|
+
#playing: boolean = false;
|
|
25
|
+
/** Whether the video should act as a background, covering its container. */
|
|
26
|
+
#background: boolean = false;
|
|
27
|
+
/** Whether the video should loop. */
|
|
28
|
+
#loop: boolean = false;
|
|
29
|
+
/** Whether to preload the video. Values: 'none', 'metadata', 'auto' */
|
|
30
|
+
#preload: "" | "none" | "metadata" | "auto" = "metadata";
|
|
31
|
+
|
|
32
|
+
private posterClickHandler: EventListener | null = null;
|
|
33
|
+
private keyboardHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
34
|
+
private ariaLiveRegion: HTMLElement | null = null;
|
|
35
|
+
private intersectionObserver: IntersectionObserver | null = null;
|
|
36
|
+
private hasLoadedVideo = false;
|
|
37
|
+
|
|
38
|
+
static get observedAttributes() {
|
|
39
|
+
return [
|
|
40
|
+
"url",
|
|
41
|
+
"autoplay",
|
|
42
|
+
"controls",
|
|
43
|
+
"lazy",
|
|
44
|
+
"muted",
|
|
45
|
+
"poster",
|
|
46
|
+
"background",
|
|
47
|
+
"loop",
|
|
48
|
+
"preload",
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
attributeChangedCallback(
|
|
53
|
+
name: string,
|
|
54
|
+
oldValue: string | null,
|
|
55
|
+
newValue: string | null,
|
|
56
|
+
) {
|
|
57
|
+
if (this.updatingAttribute) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.log(
|
|
62
|
+
`VideoEmbed: Attribute changed - ${name}: ${oldValue} -> ${newValue}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
switch (name) {
|
|
66
|
+
case "url":
|
|
67
|
+
this.#url = newValue || "";
|
|
68
|
+
if (this.initialized) {
|
|
69
|
+
this.reinitializePlayer();
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "autoplay":
|
|
73
|
+
this.#autoplay = newValue !== null;
|
|
74
|
+
if (
|
|
75
|
+
!this.#playing &&
|
|
76
|
+
this.#autoplay &&
|
|
77
|
+
!this.#lazy &&
|
|
78
|
+
this.initialized
|
|
79
|
+
) {
|
|
80
|
+
this.play();
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case "controls":
|
|
84
|
+
this.#controls = newValue !== null;
|
|
85
|
+
if (this.video) {
|
|
86
|
+
this.video.controls = this.#controls;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "lazy":
|
|
90
|
+
this.#lazy = newValue !== null;
|
|
91
|
+
break;
|
|
92
|
+
case "muted":
|
|
93
|
+
this.#muted = newValue !== null;
|
|
94
|
+
if (this.video) {
|
|
95
|
+
this.video.muted = this.#muted;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case "poster":
|
|
99
|
+
this.#poster = newValue || "";
|
|
100
|
+
if (this.#lazy && !this.video) {
|
|
101
|
+
this.showPoster();
|
|
102
|
+
} else if (this.video) {
|
|
103
|
+
this.video.poster = this.#poster;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "background":
|
|
107
|
+
this.#background = newValue !== null;
|
|
108
|
+
this.#updateBackgroundMode();
|
|
109
|
+
break;
|
|
110
|
+
case "loop":
|
|
111
|
+
this.#loop = newValue !== null;
|
|
112
|
+
if (this.video) {
|
|
113
|
+
this.video.loop = this.#loop;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case "preload":
|
|
117
|
+
const validPreloadValues: Array<"" | "none" | "metadata" | "auto"> = [
|
|
118
|
+
"none",
|
|
119
|
+
"metadata",
|
|
120
|
+
"auto",
|
|
121
|
+
"",
|
|
122
|
+
];
|
|
123
|
+
this.#preload =
|
|
124
|
+
validPreloadValues.indexOf(newValue as any) !== -1
|
|
125
|
+
? (newValue as "" | "none" | "metadata" | "auto")
|
|
126
|
+
: "metadata";
|
|
127
|
+
if (this.video) {
|
|
128
|
+
this.video.preload = this.#preload;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Getters and setters for attributes
|
|
135
|
+
get url() {
|
|
136
|
+
return this.#url;
|
|
137
|
+
}
|
|
138
|
+
set url(value: string) {
|
|
139
|
+
this.#url = value;
|
|
140
|
+
this.#reflectAttribute("url", value);
|
|
141
|
+
if (this.initialized) {
|
|
142
|
+
this.reinitializePlayer();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get autoplay() {
|
|
147
|
+
return this.#autoplay;
|
|
148
|
+
}
|
|
149
|
+
set autoplay(value: boolean) {
|
|
150
|
+
this.#autoplay = value;
|
|
151
|
+
this.#reflectBooleanAttribute("autoplay", value);
|
|
152
|
+
if (!this.#playing && value && !this.#lazy && this.initialized) {
|
|
153
|
+
this.play();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get controls() {
|
|
158
|
+
return this.#controls;
|
|
159
|
+
}
|
|
160
|
+
set controls(value: boolean) {
|
|
161
|
+
this.#controls = value;
|
|
162
|
+
this.#reflectBooleanAttribute("controls", value);
|
|
163
|
+
if (this.video) {
|
|
164
|
+
this.video.controls = value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get lazy() {
|
|
169
|
+
return this.#lazy;
|
|
170
|
+
}
|
|
171
|
+
set lazy(value: boolean) {
|
|
172
|
+
this.#lazy = value;
|
|
173
|
+
this.#reflectBooleanAttribute("lazy", value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get muted() {
|
|
177
|
+
return this.#muted;
|
|
178
|
+
}
|
|
179
|
+
set muted(value: boolean) {
|
|
180
|
+
this.#muted = value;
|
|
181
|
+
this.#reflectBooleanAttribute("muted", value);
|
|
182
|
+
if (this.video) {
|
|
183
|
+
this.video.muted = value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get poster() {
|
|
188
|
+
return this.#poster;
|
|
189
|
+
}
|
|
190
|
+
set poster(value: string) {
|
|
191
|
+
this.#poster = value;
|
|
192
|
+
this.#reflectAttribute("poster", value);
|
|
193
|
+
if (this.#lazy && !this.video) {
|
|
194
|
+
this.showPoster();
|
|
195
|
+
} else if (this.video) {
|
|
196
|
+
this.video.poster = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get playing() {
|
|
201
|
+
return this.#playing;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get background() {
|
|
205
|
+
return this.#background;
|
|
206
|
+
}
|
|
207
|
+
set background(value: boolean) {
|
|
208
|
+
this.#background = value;
|
|
209
|
+
this.#reflectBooleanAttribute("background", value);
|
|
210
|
+
this.#updateBackgroundMode();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get loop() {
|
|
214
|
+
return this.#loop;
|
|
215
|
+
}
|
|
216
|
+
set loop(value: boolean) {
|
|
217
|
+
this.#loop = value;
|
|
218
|
+
this.#reflectBooleanAttribute("loop", value);
|
|
219
|
+
if (this.video) {
|
|
220
|
+
this.video.loop = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get preload() {
|
|
225
|
+
return this.#preload;
|
|
226
|
+
}
|
|
227
|
+
set preload(value: "" | "none" | "metadata" | "auto") {
|
|
228
|
+
this.#preload = value;
|
|
229
|
+
this.#reflectAttribute("preload", value);
|
|
230
|
+
if (this.video) {
|
|
231
|
+
this.video.preload = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#reflectBooleanAttribute(name: string, value: boolean) {
|
|
236
|
+
this.updatingAttribute = true;
|
|
237
|
+
if (value) {
|
|
238
|
+
this.setAttribute(name, "");
|
|
239
|
+
} else {
|
|
240
|
+
this.removeAttribute(name);
|
|
241
|
+
}
|
|
242
|
+
this.updatingAttribute = false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#reflectAttribute(name: string, value: string) {
|
|
246
|
+
this.updatingAttribute = true;
|
|
247
|
+
if (value) {
|
|
248
|
+
this.setAttribute(name, value);
|
|
249
|
+
} else {
|
|
250
|
+
this.removeAttribute(name);
|
|
251
|
+
}
|
|
252
|
+
this.updatingAttribute = false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private log(...args: any[]) {
|
|
256
|
+
if (VideoEmbed.DEBUG) {
|
|
257
|
+
console.log("VideoEmbed:", ...args);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private warn(...args: any[]) {
|
|
262
|
+
console.warn(...args);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private error(...args: any[]) {
|
|
266
|
+
console.error(...args);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private dispatchCustomEvent(eventName: string, detail?: any) {
|
|
270
|
+
this.dispatchEvent(
|
|
271
|
+
new CustomEvent(eventName, {
|
|
272
|
+
detail,
|
|
273
|
+
bubbles: true,
|
|
274
|
+
composed: true,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private setupFullscreenListener(): void {
|
|
280
|
+
const handleFullscreenChange = (event: Event) => {
|
|
281
|
+
// Only respond to native browser fullscreen events, not our custom events
|
|
282
|
+
if (!event.isTrusted) return;
|
|
283
|
+
|
|
284
|
+
const doc = document as any;
|
|
285
|
+
const fullscreenElement =
|
|
286
|
+
doc.fullscreenElement ||
|
|
287
|
+
doc.webkitFullscreenElement ||
|
|
288
|
+
doc.mozFullScreenElement ||
|
|
289
|
+
doc.msFullscreenElement;
|
|
290
|
+
|
|
291
|
+
// Handle fullscreen for both custom element and video element
|
|
292
|
+
const isFullscreen =
|
|
293
|
+
fullscreenElement === this || fullscreenElement === this.video;
|
|
294
|
+
const wasFullscreen = this.classList.contains("is-fullscreen");
|
|
295
|
+
|
|
296
|
+
// Only dispatch event and update class if fullscreen state changed
|
|
297
|
+
if (isFullscreen !== wasFullscreen) {
|
|
298
|
+
this.dispatchEvent(
|
|
299
|
+
new CustomEvent("fullscreenchange", {
|
|
300
|
+
detail: { isFullscreen },
|
|
301
|
+
bubbles: false,
|
|
302
|
+
composed: false,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (isFullscreen) {
|
|
307
|
+
this.classList.add("is-fullscreen");
|
|
308
|
+
} else {
|
|
309
|
+
this.classList.remove("is-fullscreen");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
315
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
316
|
+
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
317
|
+
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private setupKeyboardHandlers(): void {
|
|
321
|
+
this.keyboardHandler = (e: KeyboardEvent) => {
|
|
322
|
+
if (!this.playerReady || (e.target as HTMLElement)?.tagName === "INPUT") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
switch (e.key.toLowerCase()) {
|
|
327
|
+
case "k":
|
|
328
|
+
case " ":
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
this.togglePlay();
|
|
331
|
+
this.announceToScreenReader(
|
|
332
|
+
this.#playing ? "Video playing" : "Video paused",
|
|
333
|
+
);
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
case "m":
|
|
337
|
+
e.preventDefault();
|
|
338
|
+
this.toggleMute();
|
|
339
|
+
this.announceToScreenReader(
|
|
340
|
+
this.#muted ? "Video muted" : "Video unmuted",
|
|
341
|
+
);
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case "f":
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
if (document.fullscreenElement) {
|
|
347
|
+
document.exitFullscreen().catch((error) => {
|
|
348
|
+
this.warn("VideoEmbed: Exit fullscreen failed:", error);
|
|
349
|
+
});
|
|
350
|
+
this.announceToScreenReader("Exited fullscreen");
|
|
351
|
+
} else {
|
|
352
|
+
this.enterFullscreen()
|
|
353
|
+
.then(() => {
|
|
354
|
+
this.announceToScreenReader("Entered fullscreen");
|
|
355
|
+
})
|
|
356
|
+
.catch((error) => {
|
|
357
|
+
this.warn("VideoEmbed: Enter fullscreen failed:", error);
|
|
358
|
+
this.announceToScreenReader("Fullscreen not available");
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
this.addEventListener("keydown", this.keyboardHandler as EventListener);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private announceToScreenReader(message: string): void {
|
|
369
|
+
if (this.ariaLiveRegion) {
|
|
370
|
+
this.ariaLiveRegion.textContent = message;
|
|
371
|
+
setTimeout(() => {
|
|
372
|
+
if (this.ariaLiveRegion) {
|
|
373
|
+
this.ariaLiveRegion.textContent = "";
|
|
374
|
+
}
|
|
375
|
+
}, 1000);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private setupIntersectionObserver(): void {
|
|
380
|
+
if (!("IntersectionObserver" in window)) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const options: IntersectionObserverInit = {
|
|
385
|
+
root: null,
|
|
386
|
+
rootMargin: "50px",
|
|
387
|
+
threshold: 0.01,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
391
|
+
entries.forEach((entry) => {
|
|
392
|
+
if (entry.isIntersecting && !this.hasLoadedVideo) {
|
|
393
|
+
this.hasLoadedVideo = true;
|
|
394
|
+
this.log("VideoEmbed: Video entering viewport, loading...");
|
|
395
|
+
|
|
396
|
+
const shouldAutoplay =
|
|
397
|
+
this.getAttribute("data-poster-autoplay") === "true";
|
|
398
|
+
|
|
399
|
+
if (!shouldAutoplay) {
|
|
400
|
+
this.initializePlayer();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (this.intersectionObserver) {
|
|
404
|
+
this.intersectionObserver.disconnect();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}, options);
|
|
409
|
+
|
|
410
|
+
this.intersectionObserver.observe(this);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private showErrorMessage(message: string): void {
|
|
414
|
+
const errorDiv = document.createElement("div");
|
|
415
|
+
errorDiv.className = "video-error-message";
|
|
416
|
+
errorDiv.setAttribute("role", "alert");
|
|
417
|
+
errorDiv.innerHTML = `
|
|
418
|
+
<div class="error-content">
|
|
419
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
420
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
421
|
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
422
|
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
423
|
+
</svg>
|
|
424
|
+
<p class="error-message">${message}</p>
|
|
425
|
+
<button class="retry-button">Retry</button>
|
|
426
|
+
</div>
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
const retryButton = errorDiv.querySelector(".retry-button");
|
|
430
|
+
if (retryButton) {
|
|
431
|
+
retryButton.addEventListener("click", () => {
|
|
432
|
+
errorDiv.remove();
|
|
433
|
+
if (this.#url) {
|
|
434
|
+
this.initializePlayer();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.innerHTML = "";
|
|
440
|
+
this.appendChild(errorDiv);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
connectedCallback() {
|
|
444
|
+
VideoEmbed.instanceCount++;
|
|
445
|
+
this.log(
|
|
446
|
+
"VideoEmbed: connectedCallback called, instance count:",
|
|
447
|
+
VideoEmbed.instanceCount,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const urlAttr = this.getAttribute("url");
|
|
451
|
+
if (urlAttr) {
|
|
452
|
+
this.#url = urlAttr;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.#autoplay = this.hasAttribute("autoplay");
|
|
456
|
+
this.#controls = this.hasAttribute("controls");
|
|
457
|
+
this.#lazy = this.hasAttribute("lazy");
|
|
458
|
+
this.#muted = this.hasAttribute("muted");
|
|
459
|
+
this.#background = this.hasAttribute("background");
|
|
460
|
+
this.#loop = this.hasAttribute("loop");
|
|
461
|
+
this.#poster = this.getAttribute("poster") || "";
|
|
462
|
+
const preloadAttr = this.getAttribute("preload");
|
|
463
|
+
this.#preload =
|
|
464
|
+
preloadAttr === "none" ||
|
|
465
|
+
preloadAttr === "metadata" ||
|
|
466
|
+
preloadAttr === "auto"
|
|
467
|
+
? preloadAttr
|
|
468
|
+
: "metadata";
|
|
469
|
+
|
|
470
|
+
if (!this.#url) {
|
|
471
|
+
this.error("VideoEmbed: Missing video URL");
|
|
472
|
+
this.dispatchCustomEvent("error", {
|
|
473
|
+
message: "Missing video URL",
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.classList.add("video-embed-wrapper");
|
|
479
|
+
|
|
480
|
+
this.setAttribute("role", "region");
|
|
481
|
+
this.setAttribute("aria-label", "Video player");
|
|
482
|
+
if (!this.hasAttribute("tabindex")) {
|
|
483
|
+
this.setAttribute("tabindex", "0");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.ariaLiveRegion = document.createElement("div");
|
|
487
|
+
this.ariaLiveRegion.setAttribute("aria-live", "polite");
|
|
488
|
+
this.ariaLiveRegion.setAttribute("aria-atomic", "true");
|
|
489
|
+
this.ariaLiveRegion.className = "sr-only";
|
|
490
|
+
this.ariaLiveRegion.style.cssText =
|
|
491
|
+
"position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;";
|
|
492
|
+
this.appendChild(this.ariaLiveRegion);
|
|
493
|
+
|
|
494
|
+
this.setupKeyboardHandlers();
|
|
495
|
+
this.setupFullscreenListener();
|
|
496
|
+
|
|
497
|
+
this.#updateBackgroundMode();
|
|
498
|
+
|
|
499
|
+
if (this.#lazy) {
|
|
500
|
+
this.showPoster();
|
|
501
|
+
this.setupIntersectionObserver();
|
|
502
|
+
} else {
|
|
503
|
+
this.initializePlayer();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.initialized = true;
|
|
507
|
+
this.dispatchCustomEvent("connected");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
disconnectedCallback() {
|
|
511
|
+
VideoEmbed.instanceCount--;
|
|
512
|
+
this.log(
|
|
513
|
+
"VideoEmbed: disconnectedCallback called, remaining instances:",
|
|
514
|
+
VideoEmbed.instanceCount,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (this.video) {
|
|
518
|
+
this.video.pause();
|
|
519
|
+
this.video.src = "";
|
|
520
|
+
this.video.load();
|
|
521
|
+
this.video = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (this.keyboardHandler) {
|
|
525
|
+
this.removeEventListener(
|
|
526
|
+
"keydown",
|
|
527
|
+
this.keyboardHandler as EventListener,
|
|
528
|
+
);
|
|
529
|
+
this.keyboardHandler = null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const poster = this.querySelector(".video-poster");
|
|
533
|
+
const buttonOverlay = this.querySelector(".button-overlay");
|
|
534
|
+
|
|
535
|
+
if (poster && this.posterClickHandler) {
|
|
536
|
+
poster.removeEventListener("click", this.posterClickHandler);
|
|
537
|
+
this.posterClickHandler = null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (buttonOverlay && this.posterClickHandler) {
|
|
541
|
+
buttonOverlay.removeEventListener("click", this.posterClickHandler);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (this.intersectionObserver) {
|
|
545
|
+
this.intersectionObserver.disconnect();
|
|
546
|
+
this.intersectionObserver = null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.innerHTML = "";
|
|
550
|
+
this.classList.remove(
|
|
551
|
+
"video-embed-wrapper",
|
|
552
|
+
"video-embed-container",
|
|
553
|
+
"is-playing",
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
this.playPauseButton = null;
|
|
557
|
+
this.setCustomControlState = null;
|
|
558
|
+
|
|
559
|
+
this.dispatchCustomEvent("disconnected");
|
|
560
|
+
|
|
561
|
+
this.log("VideoEmbed: Cleanup complete.");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private reinitializePlayer() {
|
|
565
|
+
if (!this.initialized || !this.#url) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (this.video) {
|
|
570
|
+
this.video.pause();
|
|
571
|
+
// Remove the video element instead of clearing src to avoid error events
|
|
572
|
+
this.video.remove();
|
|
573
|
+
this.video = null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Clear player state
|
|
577
|
+
this.playerReady = false;
|
|
578
|
+
this.#playing = false;
|
|
579
|
+
this.classList.remove("is-playing");
|
|
580
|
+
|
|
581
|
+
if (this.#lazy) {
|
|
582
|
+
this.showPoster();
|
|
583
|
+
} else {
|
|
584
|
+
this.initializePlayer();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private showPoster() {
|
|
589
|
+
const posterUrl = this.#poster || "";
|
|
590
|
+
|
|
591
|
+
this.log("VideoEmbed: Using poster URL:", posterUrl);
|
|
592
|
+
|
|
593
|
+
this.innerHTML = "";
|
|
594
|
+
|
|
595
|
+
if (this.posterClickHandler) {
|
|
596
|
+
this.posterClickHandler = null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const poster = document.createElement("div");
|
|
600
|
+
poster.classList.add("video-poster");
|
|
601
|
+
poster.setAttribute("role", "img");
|
|
602
|
+
poster.setAttribute("aria-label", "Video thumbnail");
|
|
603
|
+
|
|
604
|
+
if (posterUrl) {
|
|
605
|
+
poster.style.backgroundImage = `url('${posterUrl}')`;
|
|
606
|
+
} else {
|
|
607
|
+
poster.style.backgroundColor = "#000000";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const buttonOverlay = document.createElement("div");
|
|
611
|
+
buttonOverlay.classList.add("button-overlay");
|
|
612
|
+
buttonOverlay.setAttribute("role", "button");
|
|
613
|
+
buttonOverlay.setAttribute("tabindex", "0");
|
|
614
|
+
buttonOverlay.setAttribute("aria-label", "Play video");
|
|
615
|
+
|
|
616
|
+
const button = document.createElement("div");
|
|
617
|
+
button.classList.add("button");
|
|
618
|
+
button.setAttribute("aria-hidden", "true");
|
|
619
|
+
|
|
620
|
+
buttonOverlay.appendChild(button);
|
|
621
|
+
|
|
622
|
+
const loadVideo: EventListener = () => {
|
|
623
|
+
this.log("VideoEmbed: Loading video from poster click");
|
|
624
|
+
this.setAttribute("data-poster-autoplay", "true");
|
|
625
|
+
try {
|
|
626
|
+
this.initializePlayer();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.error("VideoEmbed: Error initializing player from poster:", error);
|
|
629
|
+
this.dispatchCustomEvent("error", { message: "Failed to load video" });
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
this.posterClickHandler = loadVideo;
|
|
634
|
+
|
|
635
|
+
const keyboardActivate = (e: KeyboardEvent) => {
|
|
636
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
637
|
+
e.preventDefault();
|
|
638
|
+
loadVideo(e);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
poster.addEventListener("click", loadVideo);
|
|
643
|
+
buttonOverlay.addEventListener("click", loadVideo);
|
|
644
|
+
buttonOverlay.addEventListener(
|
|
645
|
+
"keydown",
|
|
646
|
+
keyboardActivate as EventListener,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
this.appendChild(poster);
|
|
650
|
+
this.appendChild(buttonOverlay);
|
|
651
|
+
|
|
652
|
+
this.log("VideoEmbed: Poster displayed for lazy loading");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private initializePlayer() {
|
|
656
|
+
this.log("VideoEmbed: Initializing player for URL:", this.#url);
|
|
657
|
+
|
|
658
|
+
const isBackground = this.#background;
|
|
659
|
+
const autoplay =
|
|
660
|
+
isBackground ||
|
|
661
|
+
this.hasAttribute("autoplay") ||
|
|
662
|
+
this.hasAttribute("data-poster-autoplay") ||
|
|
663
|
+
this.hasAttribute("data-should-autoplay");
|
|
664
|
+
const controls = isBackground ? false : this.hasAttribute("controls");
|
|
665
|
+
const mute = isBackground || autoplay;
|
|
666
|
+
|
|
667
|
+
if (this.hasAttribute("data-poster-autoplay")) {
|
|
668
|
+
this.removeAttribute("data-poster-autoplay");
|
|
669
|
+
}
|
|
670
|
+
if (this.hasAttribute("data-should-autoplay")) {
|
|
671
|
+
this.removeAttribute("data-should-autoplay");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (autoplay && !this.#muted) {
|
|
675
|
+
this.#muted = true;
|
|
676
|
+
this.log(
|
|
677
|
+
"VideoEmbed: Autoplay enabled, forcing muted state for compliance",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.innerHTML = "";
|
|
682
|
+
|
|
683
|
+
this.video = document.createElement("video");
|
|
684
|
+
// Let CSS handle positioning and sizing
|
|
685
|
+
// Only set object-fit for backward compatibility with older browsers
|
|
686
|
+
this.video.style.objectFit = this.#background ? "cover" : "contain";
|
|
687
|
+
|
|
688
|
+
this.video.src = this.#url;
|
|
689
|
+
this.video.controls = controls;
|
|
690
|
+
this.video.autoplay = autoplay;
|
|
691
|
+
this.video.muted = mute || this.#muted;
|
|
692
|
+
this.video.loop = this.#loop;
|
|
693
|
+
this.video.preload = this.#preload;
|
|
694
|
+
this.video.playsInline = true;
|
|
695
|
+
|
|
696
|
+
if (this.#poster) {
|
|
697
|
+
this.video.poster = this.#poster;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
this.setupVideoEvents();
|
|
701
|
+
|
|
702
|
+
this.appendChild(this.video);
|
|
703
|
+
|
|
704
|
+
if (!controls) {
|
|
705
|
+
this.addCustomControls();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
this.playerReady = true;
|
|
709
|
+
this.dispatchCustomEvent("ready", {
|
|
710
|
+
url: this.#url,
|
|
711
|
+
muted: this.#muted,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
if (autoplay) {
|
|
715
|
+
this.video.play().catch((error) => {
|
|
716
|
+
this.warn("VideoEmbed: Autoplay failed:", error);
|
|
717
|
+
this.dispatchCustomEvent("error", {
|
|
718
|
+
message: "Autoplay failed",
|
|
719
|
+
error,
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private setupVideoEvents() {
|
|
726
|
+
if (!this.video) return;
|
|
727
|
+
|
|
728
|
+
this.video.addEventListener("play", () => {
|
|
729
|
+
this.#playing = true;
|
|
730
|
+
this.classList.add("is-playing");
|
|
731
|
+
if (this.setCustomControlState) {
|
|
732
|
+
this.setCustomControlState(true);
|
|
733
|
+
}
|
|
734
|
+
this.dispatchCustomEvent("play", {
|
|
735
|
+
url: this.#url,
|
|
736
|
+
currentTime: this.video?.currentTime || 0,
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
this.video.addEventListener("pause", () => {
|
|
741
|
+
this.#playing = false;
|
|
742
|
+
this.classList.remove("is-playing");
|
|
743
|
+
if (this.setCustomControlState) {
|
|
744
|
+
this.setCustomControlState(false);
|
|
745
|
+
}
|
|
746
|
+
this.dispatchCustomEvent("pause", {
|
|
747
|
+
url: this.#url,
|
|
748
|
+
currentTime: this.video?.currentTime || 0,
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
this.video.addEventListener("ended", () => {
|
|
753
|
+
this.#playing = false;
|
|
754
|
+
this.classList.remove("is-playing");
|
|
755
|
+
if (this.setCustomControlState) {
|
|
756
|
+
this.setCustomControlState(false);
|
|
757
|
+
}
|
|
758
|
+
this.dispatchCustomEvent("ended", {
|
|
759
|
+
url: this.#url,
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
this.video.addEventListener("timeupdate", () => {
|
|
764
|
+
this.dispatchCustomEvent("timeupdate", {
|
|
765
|
+
url: this.#url,
|
|
766
|
+
currentTime: this.video?.currentTime || 0,
|
|
767
|
+
duration: this.video?.duration || 0,
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
this.video.addEventListener("volumechange", () => {
|
|
772
|
+
this.#muted = this.video?.muted || false;
|
|
773
|
+
this.dispatchCustomEvent("volumechange", {
|
|
774
|
+
muted: this.#muted,
|
|
775
|
+
volume: this.video?.volume || 0,
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
this.video.addEventListener("error", (e) => {
|
|
780
|
+
this.error("VideoEmbed: Video error:", e);
|
|
781
|
+
this.dispatchCustomEvent("error", {
|
|
782
|
+
message: "Video playback error",
|
|
783
|
+
error: e,
|
|
784
|
+
});
|
|
785
|
+
this.showErrorMessage(
|
|
786
|
+
"Failed to load video. Please check the URL or try again later.",
|
|
787
|
+
);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
this.video.addEventListener("loadedmetadata", () => {
|
|
791
|
+
this.dispatchCustomEvent("loadedmetadata", {
|
|
792
|
+
duration: this.video?.duration || 0,
|
|
793
|
+
videoWidth: this.video?.videoWidth || 0,
|
|
794
|
+
videoHeight: this.video?.videoHeight || 0,
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private addCustomControls() {
|
|
800
|
+
this.classList.add("video-embed-container");
|
|
801
|
+
|
|
802
|
+
const buttonOverlay = document.createElement("div");
|
|
803
|
+
buttonOverlay.classList.add("button-overlay");
|
|
804
|
+
|
|
805
|
+
this.playPauseButton = document.createElement("div");
|
|
806
|
+
this.playPauseButton.classList.add("button");
|
|
807
|
+
let isPlaying = false;
|
|
808
|
+
|
|
809
|
+
const setPlayingState = (playing: boolean) => {
|
|
810
|
+
isPlaying = playing;
|
|
811
|
+
if (playing) {
|
|
812
|
+
this.classList.add("is-playing");
|
|
813
|
+
} else {
|
|
814
|
+
this.classList.remove("is-playing");
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const togglePlayPause = () => {
|
|
819
|
+
if (this.video) {
|
|
820
|
+
if (isPlaying) {
|
|
821
|
+
this.video.pause();
|
|
822
|
+
setPlayingState(false);
|
|
823
|
+
} else {
|
|
824
|
+
this.video.play().catch((error) => {
|
|
825
|
+
this.warn("VideoEmbed: Play failed:", error);
|
|
826
|
+
});
|
|
827
|
+
setPlayingState(true);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
this.setCustomControlState = setPlayingState;
|
|
833
|
+
|
|
834
|
+
buttonOverlay.addEventListener("click", togglePlayPause);
|
|
835
|
+
if (this.playPauseButton) {
|
|
836
|
+
buttonOverlay.appendChild(this.playPauseButton);
|
|
837
|
+
}
|
|
838
|
+
this.appendChild(buttonOverlay);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
#updateBackgroundMode() {
|
|
842
|
+
if (this.#background) {
|
|
843
|
+
this.classList.add("is-background");
|
|
844
|
+
this.#autoplay = true;
|
|
845
|
+
this.#muted = true;
|
|
846
|
+
this.#controls = false;
|
|
847
|
+
this.#lazy = false;
|
|
848
|
+
// Reflect the forced property changes back to attributes
|
|
849
|
+
this.#reflectBooleanAttribute("autoplay", true);
|
|
850
|
+
this.#reflectBooleanAttribute("muted", true);
|
|
851
|
+
this.#reflectBooleanAttribute("controls", false);
|
|
852
|
+
this.#reflectBooleanAttribute("lazy", false);
|
|
853
|
+
} else {
|
|
854
|
+
this.classList.remove("is-background");
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Public API methods
|
|
859
|
+
public async play(): Promise<void> {
|
|
860
|
+
if (this.#lazy && !this.video) {
|
|
861
|
+
this.log(
|
|
862
|
+
"VideoEmbed: Lazy video needs to be loaded first. Initializing...",
|
|
863
|
+
);
|
|
864
|
+
this.setAttribute("data-should-autoplay", "true");
|
|
865
|
+
this.initializePlayer();
|
|
866
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
this.removeAttribute("data-should-autoplay");
|
|
870
|
+
|
|
871
|
+
if (this.video) {
|
|
872
|
+
try {
|
|
873
|
+
await this.video.play();
|
|
874
|
+
this.log("VideoEmbed: Video playback started.");
|
|
875
|
+
} catch (error) {
|
|
876
|
+
this.warn("VideoEmbed: Play failed:", error);
|
|
877
|
+
this.dispatchCustomEvent("error", {
|
|
878
|
+
message: "Failed to play video",
|
|
879
|
+
error,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
public pause(): void {
|
|
886
|
+
if (this.video) {
|
|
887
|
+
this.video.pause();
|
|
888
|
+
this.log("VideoEmbed: Video paused.");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
public stopVideo(): void {
|
|
893
|
+
if (this.video) {
|
|
894
|
+
this.video.pause();
|
|
895
|
+
this.video.currentTime = 0;
|
|
896
|
+
this.log("VideoEmbed: Video stopped.");
|
|
897
|
+
this.dispatchCustomEvent("stop", {
|
|
898
|
+
url: this.#url,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
public mute(): void {
|
|
904
|
+
if (this.video) {
|
|
905
|
+
this.video.muted = true;
|
|
906
|
+
this.#muted = true;
|
|
907
|
+
this.#reflectBooleanAttribute("muted", true);
|
|
908
|
+
this.log("VideoEmbed: Video muted.");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
public unmute(): void {
|
|
913
|
+
if (this.video) {
|
|
914
|
+
this.video.muted = false;
|
|
915
|
+
this.#muted = false;
|
|
916
|
+
this.#reflectBooleanAttribute("muted", false);
|
|
917
|
+
this.log("VideoEmbed: Video unmuted.");
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
public togglePlay(): void {
|
|
922
|
+
if (this.#playing) {
|
|
923
|
+
this.pause();
|
|
924
|
+
} else {
|
|
925
|
+
this.play();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
public toggleMute(): void {
|
|
930
|
+
if (this.#muted) {
|
|
931
|
+
this.unmute();
|
|
932
|
+
} else {
|
|
933
|
+
this.mute();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
public loadVideo(url?: string): void {
|
|
938
|
+
if (url) {
|
|
939
|
+
this.#url = url;
|
|
940
|
+
this.#reflectAttribute("url", url);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (this.#lazy && !this.video) {
|
|
944
|
+
this.initializePlayer();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (this.initialized) {
|
|
949
|
+
this.reinitializePlayer();
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
public getPlayerState() {
|
|
954
|
+
return {
|
|
955
|
+
playing: this.#playing,
|
|
956
|
+
muted: this.#muted,
|
|
957
|
+
url: this.#url,
|
|
958
|
+
ready: this.playerReady,
|
|
959
|
+
initialized: this.initialized,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
public getCurrentTime(): number {
|
|
964
|
+
return this.video?.currentTime || 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
public getDuration(): number {
|
|
968
|
+
return this.video?.duration || 0;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
public setCurrentTime(time: number): void {
|
|
972
|
+
if (this.video) {
|
|
973
|
+
this.video.currentTime = time;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
public setVolume(volume: number): void {
|
|
978
|
+
if (this.video) {
|
|
979
|
+
this.video.volume = Math.max(0, Math.min(1, volume));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
public getVolume(): number {
|
|
984
|
+
return this.video?.volume || 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
public isPlaying(): boolean {
|
|
988
|
+
return this.#playing;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
public isMuted(): boolean {
|
|
992
|
+
return this.#muted;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
public enterFullscreen(): Promise<void> {
|
|
996
|
+
// Use the video element for fullscreen to be compatible with native controls
|
|
997
|
+
const elem = (this.video || this) as any;
|
|
998
|
+
if (elem.requestFullscreen) {
|
|
999
|
+
return elem.requestFullscreen();
|
|
1000
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
1001
|
+
return elem.webkitRequestFullscreen();
|
|
1002
|
+
} else if (elem.mozRequestFullScreen) {
|
|
1003
|
+
return elem.mozRequestFullScreen();
|
|
1004
|
+
} else if (elem.msRequestFullscreen) {
|
|
1005
|
+
return elem.msRequestFullscreen();
|
|
1006
|
+
}
|
|
1007
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
public exitFullscreen(): Promise<void> {
|
|
1011
|
+
const doc = document as any;
|
|
1012
|
+
if (doc.exitFullscreen) {
|
|
1013
|
+
return doc.exitFullscreen();
|
|
1014
|
+
} else if (doc.webkitExitFullscreen) {
|
|
1015
|
+
return doc.webkitExitFullscreen();
|
|
1016
|
+
} else if (doc.mozCancelFullScreen) {
|
|
1017
|
+
return doc.mozCancelFullScreen();
|
|
1018
|
+
} else if (doc.msExitFullscreen) {
|
|
1019
|
+
return doc.msExitFullscreen();
|
|
1020
|
+
}
|
|
1021
|
+
return Promise.reject(new Error("Fullscreen API not supported"));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
public async toggleFullscreen(): Promise<void> {
|
|
1025
|
+
try {
|
|
1026
|
+
if (this.isFullscreen()) {
|
|
1027
|
+
await this.exitFullscreen();
|
|
1028
|
+
} else {
|
|
1029
|
+
await this.enterFullscreen();
|
|
1030
|
+
}
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
this.warn("VideoEmbed: Toggle fullscreen failed:", error);
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
public isFullscreen(): boolean {
|
|
1038
|
+
const doc = document as any;
|
|
1039
|
+
const fullscreenElement =
|
|
1040
|
+
doc.fullscreenElement ||
|
|
1041
|
+
doc.webkitFullscreenElement ||
|
|
1042
|
+
doc.mozFullScreenElement ||
|
|
1043
|
+
doc.msFullscreenElement;
|
|
1044
|
+
// Check if either the custom element or video element is fullscreen
|
|
1045
|
+
return fullscreenElement === this || fullscreenElement === this.video;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
customElements.define("video-embed", VideoEmbed);
|
|
1050
|
+
|
|
1051
|
+
export {};
|