PageSourceSearch

https://www.martinpublicschools.org/apps-shared/video_player/envideo.js

js martinpublicschools.org collected 2026-10-05 08:31:51 UTC 40,022 bytes, 1,131 lines download raw bytes

1class ENVideo extends HTMLElement {
2
3    subtitle = {}
4
5    CONTROLLER_PADDING_INLINE = 12;
6    HORIZONTAL_PREVIEW_HEIGHT_RATIO = 0.4533333;
7    VERTICAL_PREVIEW_HEIGHT_RATIO = 0.47;
8
9    currentPlatform;
10    currentBrowser;
11
12    selectedLang = "en";
13    focusedOnMe = false;
14
15    static mpd_source_tag = "data-mpd-src";
16    static m3u8_source_tag = "data-m3u8-src";
17    sourceType = "";
18
19    mainVideoLoaded = false;
20    previewVideoLoaded = false;
21
22    isDebugMode = false;
23
24    constructor() {
25        super();
26        this.attachShadow({mode: 'open'});
27
28        this.isDebugMode = /[?&]debug=(true|1)/.test(window.location.search);
29    }
30
31    loadStyles() {
32        this.style.display = "none";
33        const link = document.createElement('link');
34        link.rel = 'stylesheet';
35        link.href = "/apps-shared/video_player/envideo.css";
36
37        link.onload = () => {
38            this.style.display = "inline-block";
39        }
40        link.onerror = () => {
41            console.log("Error loading video player styles");
42        }
43        this.shadowRoot.innerHTML = enVideoUtils.innerHTML;
44        this.shadowRoot.prepend(link);
45
46        const checkStylesLoaded = () => {
47            try {
48                for (const sheet of this.shadowRoot.styleSheets || []) {
49                    if (sheet.href && sheet.href.includes('/video_player/envideo.css')) {
50                        this.style.display = "inline-block";
51                        return;
52                    }
53                }
54            } catch (e) {
55            }
56
57            setTimeout(checkStylesLoaded, 50);
58        };
59
60        checkStylesLoaded();
61    }
62
63
64    connectedCallback() {
65        this.loadStyles();
66        this.setupElements();
67        this.detectCurrentBrowser();
68        this.detectCurrentPlatform();
69        this.log(`CurrentBrowser: ${this.currentBrowser} - CurrentPlatform: ${this.currentPlatform}`);
70        if (this.videoPlayer) {
71            this.setPlayStatus(enVideoUtils.playStatus.pause);
72
73            this.initAttributes();
74            this.initEventListeners();
75            this.initSource();
76            this.initPreviewVideo();
77            this.initSubtitles();
78            this.showControlContainer();
79            this.dispatchEvent(new Event('onload'));
80
81            this._resizeObserver = new ResizeObserver(entries => {
82                for (let entry of entries) {
83                    if (entry.contentRect) {
84                        this.setVideoMode();
85                        this.adjustPreviewSize();
86                    }
87                }
88            });
89            this._resizeObserver.observe(this);
90        }
91    }
92
93    disconnectedCallback() {
94        this._resizeObserver.unobserve(this);
95    }
96
97    static get observedAttributes() {
98        return ['src', 'autoplay', 'muted', "poster", ENVideo.mpd_source_tag, ENVideo.m3u8_source_tag, "data-src"];
99    }
100
101    attributeChangedCallback(name, oldValue, newValue) {
102        if (!this.videoPlayer) {
103            return;
104        }
105        this.log(`Attribute changed: ${name}`);
106        if (('src' === name || "data-src" === name) && newValue) {
107            this.addSource(newValue, this.getMimeTypeFromSrc(newValue))
108        } else if ('autoplay' === name) {
109            this.initAttribute("autoplay");
110        } else if ('muted' === name) {
111            this.initAttribute("muted")
112        } else if ('loop' === name) {
113            this.initAttribute("loop");
114        } else if ("poster" === name) {
115            this.initPoster(newValue);
116        } else if (ENVideo.mpd_source_tag === name && newValue !== null) {
117            if (this.dataset.mpdSrc) {
118                this.initMpdSource();
119            }
120        } else if (ENVideo.m3u8_source_tag === name && newValue !== null) {
121            if (this.dataset.m3u8Src) {
122                this.initM3U8Source();
123            }
124        }
125    }
126
127    initPoster = (url) => {
128        if (typeof url !== "undefined" && url !== undefined && url !== null) {
129            this.videoEl.poster = url;
130        }
131    }
132
133
134    initAttributes() {
135        this.initAttribute("muted");
136        this.initAttribute("loop");
137        this.initAttribute("autoplay")
138        this.initPoster(this.getAttribute("poster"));
139
140        const preload = this.getAttribute("preload");
141        this.videoEl.preload = 'auto' === preload ? 'auto' : 'metadata';
142
143        this.videoEl.setAttribute('playsinline', '');
144        this.videoEl.playsInline = true;
145        this.videoEl.disablePictureInPicture = true;
146    }
147
148    initAttribute = (attr) => {
149        if (this.hasAttribute(attr)) {
150            const value = this.getAttribute(attr);
151            this.videoEl[attr] = value !== false && value?.toLowerCase() !== 'false';
152        }
153    }
154
155    getSrc = () => {
156        return this.dataset.src || this.getAttribute('src');
157    }
158
159    initSource() {
160        try {
161            if (this.dataset.m3u8Src && enVideoUtils.Browser.Safari === this.currentBrowser && this.videoEl.canPlayType('application/vnd.apple.mpegurl')) {
162                this.initM3U8Source();
163            } else if (this.dataset.mpdSrc) {
164                this.initMpdSource();
165            } else if (this.getSrc()) {
166                const src = this.getSrc();
167                this.addSource(src, this.getMimeTypeFromSrc(src));
168            } else {
169                this.handleSourceError(new Error("This video source is not available"));
170            }
171        } catch (e) {
172            console.error(e);
173        }
174    }
175
176    getMimeTypeFromSrc(src) {
177        const extension = src.split('.').pop().toLowerCase();
178        const mimeTypes = {
179            mp4: 'video/mp4',
180            webm: 'video/webm',
181            ogv: 'video/ogg',
182            avi: 'video/x-msvideo',
183            mov: 'video/quicktime',
184            mkv: 'video/x-matroska'
185        };
186        return mimeTypes[extension] || mimeTypes['mp4'];
187    }
188
189
190    addSource(src, type) {
191        try {
192            this.source = src;
193            const source = document.createElement('source');
194            source.src = src;
195            source.type = type;
196            source.addEventListener('error', this.handleSourceError);
197            this.videoEl.appendChild(source);
198
199            const previewSource = document.createElement('source');
200            previewSource.src = src;
201            previewSource.type = type;
202            this.previewVideo.appendChild(previewSource);
203        } catch (e) {
204            this.log(`error while adding source: ${e}`);
205        }
206    }
207
208    handleSourceError(e) {
209        if (this.getSrc() && ("m3u8" === this.sourceType || "mpd" === this.sourceType)) {
210            this.videoEl.src = this.getSrc();
211            this.previewVideo.src = this.getSrc();
212        } else {
213            this.spinner.style.display = 'none';
214            this.videoAvailableMessage.style.display = "flex";
215            this.controlContainer.style.display = "none";
216        }
217        console.error(e);
218    }
219
220    qs(sel) {
221        return this.shadowRoot.querySelector(sel);
222    }
223
224
225    isEmpty(val) {
226        return val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0);
227    }
228
229    setupElements() {
230        this.videoPlayer = this.qs("#envideo");
231        this.videoEl = this.qs("#mainVideo");
232        this.controlContainer = this.qs('.video-controller');
233        this.playBtn = this.qs('.en-vp-control-play');
234        this.pauseBtn = this.qs('.en-vp-control-pause');
235        this.restartBtn = this.qs('.en-vp-control-restart');
236        this.muteBtn = this.qs('.en-vp-control-mute');
237        this.unmuteBtn = this.qs('.en-vp-control-unmute');
238        this.progressBarContainer = this.qs('.progress-bar-container');
239        this.progressBar = this.qs('.progress-bar');
240        this.progressIndicator = this.qs('.progress-indicator');
241        this.currentTime = this.qs('.current-time');
242        this.totalTime = this.qs('.total-time');
243        this.volumeEl = this.qs('.sound-volume');
244        this.settingsBtn = this.qs('.en-vp-control-settings');
245        this.settingsBox = this.qs('.player-options-wrapper');
246        this.playSpeed = this.qs('#play-speed');
247        this.fullScreenBtn = this.qs('.en-vp-control-fullscreen');
248        this.fullScreenExitBtn = this.qs('.en-vp-control-fullscreen-exit');
249        this.spinner = this.qs('.en-spinner');
250        this.subTitleBtn = this.qs('.en-vp-control-subtitle');
251        this.subTitleText = this.qs('.subtitle-container > span');
252        this.previewContainer = this.qs('#preview-container');
253        this.previewVideo = this.qs('#preview-container video');
254        this.previewCanvas = this.qs('#preview-container canvas');
255        this.previewTimeSpan = this.qs('#preview-container span')
256        this.previewCanvasContext = this.previewCanvas.getContext('2d');
257        this.middleBox = this.qs('.middle-box');
258        this.middleBoxIcon = this.qs('.middle-box-icon');
259        this.videoAvailableMessage = this.qs('#video-available-message');
260    }
261
262    async startVideo() {
263        this.videoEl.time = 0;
264        await this.play();
265    }
266
267    play() {
268        try {
269            if (!this.mainVideoLoaded) {
270                return;
271            }
272            this.videoEl.play();
273        } catch (e) {
274            console.error("Error Playing Video", e);
275        }
276    }
277
278    doWhenVideoPlay() {
279        try {
280            this.toggleDisplay(this.playBtn, this.pauseBtn);
281            this.hide(this.restartBtn)
282            this.setPlayStatus(enVideoUtils.playStatus.play);
283            this.middleBoxIcon.innerHTML = enVideoUtils.icons.playIcon;
284            this.animateMiddleIcon();
285        } catch (e) {
286            console.error("Error Playing Video", e);
287        }
288    }
289
290    pause() {
291        this.videoEl.pause();
292    }
293
294    doWhenVideoPause() {
295        this.toggleDisplay(this.pauseBtn, this.playBtn);
296        this.setPlayStatus(enVideoUtils.playStatus.pause);
297        this.middleBoxIcon.innerHTML = enVideoUtils.icons.pauseIcon;
298        this.animateMiddleIcon();
299    }
300
301    setPlayStatus(playStatus) {
302        this.playStatus = playStatus;
303        this.setAttribute("play-status", playStatus);
304    }
305
306    toggleDisplay(hide, display) {
307        this.hide(hide);
308        this.show(display);
309    }
310
311    mute() {
312        this.holdedVolume = this.volumeEl.value;
313        this.videoEl.muted = true;
314        this.doWhenVideoMute();
315    }
316
317    doWhenVideoMute() {
318        this.toggleDisplay(this.muteBtn, this.unmuteBtn);
319        this.log(`Video Muted volume.value: ${this.volumeEl.value}`);
320        this.volumeEl.value = 0;
321        this.videoPlayer.classList.add('muted');
322    }
323
324    unmute() {
325        this.videoEl.muted = false;
326        this.doWhenVideoUnmute();
327    }
328
329    doWhenVideoUnmute() {
330        this.toggleDisplay(this.unmuteBtn, this.muteBtn);
331        this.volumeEl.value = this.holdedVolume;
332        this.videoPlayer.classList.remove('muted');
333    }
334
335    videoDuration(d) {
336        let sec = Math.round(d % 60);
337        let min = Math.floor(d / 60);
338        const hr = min > 59 ? Math.floor(min / 60) : 0;
339
340        min = min % 60;
341        min = min < 10 ? '0' + min : min;
342        sec = sec < 10 ? '0' + sec : sec;
343
344        return hr > 0 ? `${hr}:${min}:${sec}` : `${min}:${sec}`;
345    }
346
347    initEventListeners() {
348        this.addVideoPlayerEventListeners();
349        this.addControlButtonEventListeners();
350        this.addVideoElementEventListeners();
351        this.addDocumentEventListeners();
352    }
353
354    addVideoPlayerEventListeners() {
355        this.addEventListener('click', () => {
356            this.focus();
357            this.focusedOnMe = true;
358        });
359        this.addEventListener('focus', () => {
360            this.focusedOnMe = true;
361            this.showControlContainer();
362        });
363        this.addEventListener('blur', () => {
364            this.focusedOnMe = false;
365        });
366        this.addEventListener('mouseenter', this.showControlContainer.bind(this));
367
368        this.addEventListener('mouseleave', this.hideControlContainer.bind(this));
369
370    }
371
372    addControlButtonEventListeners() {
373        this.playBtn.addEventListener('click', () => this.startVideo());
374        this.pauseBtn.addEventListener('click', () => this.pause());
375        this.muteBtn.addEventListener('click', () => this.mute());
376        this.unmuteBtn.addEventListener('click', () => this.unmute());
377        this.playSpeed.addEventListener('change', (e) => this.handlePlaySpeed(e));
378        this.restartBtn.addEventListener('click', () => this.handleRestart());
379        this.settingsBtn.addEventListener('click', (e) => this.toggleSettingsMenu(e));
380        this.fullScreenBtn.addEventListener('click', () => this.handleFullScreen());
381        this.fullScreenExitBtn.addEventListener('click', () => this.handleExitFullScreen());
382        this.progressBarContainer.addEventListener('click', (e) => this.handleProgressBarClick(e));
383        this.progressBar.addEventListener('click', (e) => this.handleProgressBarClick(e));
384        this.subTitleBtn.addEventListener('click', (e) => this.toggleSubtitle());
385    }
386
387    addVideoElementEventListeners() {
388        const eventsToMonitor = ['progress', 'canplaythrough', 'playing', 'stalled', 'waiting', 'loadedmetadata'];
389
390        ['play'].forEach(eventName => {
391            this.videoEl.addEventListener(eventName, (e) => {
392                const newEvent = new Event(eventName, { bubbles: true });
393                this.dispatchEvent(newEvent);
394            });
395        });
396
397        const showSpinnerDebounced = enVideoUtils.debounce(() => this.show(this.spinner), 100);
398        const hideSpinnerDebounced = enVideoUtils.debounce(() => this.hide(this.spinner), 100);
399
400        const isMetadataOnlyPreload = this.videoEl.getAttribute('preload') === 'metadata';
401
402        eventsToMonitor.forEach(event => {
403            this.videoEl.addEventListener(event, () => {
404                this.log(`Event: ${event}, readyState: ${this.videoEl.readyState}`);
405
406                switch (this.videoEl.readyState) {
407                    case 0: // HAVE_NOTHING
408                        showSpinnerDebounced();
409                        break;
410                    case 1: // HAVE_METADATA
411                        hideSpinnerDebounced();
412                        this.mainVideoLoaded = true;
413                        break;
414                    case 2: // HAVE_CURRENT_DATA
415                        showSpinnerDebounced();
416                        break;
417                    case 3: // HAVE_FUTURE_DATA
418                    case 4: // HAVE_ENOUGH_DATA
419                        hideSpinnerDebounced();
420                        this.mainVideoLoaded = true;
421                        break;
422                    default:
423                        this.log("Unknown readyState value:", this.videoEl.readyState);
424                }
425            });
426        });
427
428        this.videoEl.addEventListener('loadstart', () => {
429            showSpinnerDebounced();
430        });
431
432        this.videoEl.addEventListener('canplay', () => {
433            this.mainVideoLoaded = true;
434            this.setVideoMode();
435            hideSpinnerDebounced();
436        });
437
438        this.videoEl.addEventListener('error', (e) => {
439            this.log(e);
440            this.show(this.spinner);
441        });
442
443        this.videoEl.addEventListener('seeking', () => {
444            showSpinnerDebounced();
445        });
446
447        this.videoEl.addEventListener('seeked', () => {
448            hideSpinnerDebounced();
449        });
450
451        this.videoEl.addEventListener('timeupdate', () => {
452            this.currentTime.innerHTML = this.videoDuration(this.videoEl.currentTime);
453            this.progressIndicator.style.width = (this.videoEl.currentTime / this.videoEl.duration) * 100 + '%';
454        });
455
456        this.videoEl.addEventListener('ended', this.handleVideoEnded.bind(this));
457
458        this.videoEl.addEventListener("play", () => this.doWhenVideoPlay());
459        this.videoEl.addEventListener("pause", () => this.doWhenVideoPause());
460
461        this.volumeEl.addEventListener('change', this.toggleMuteButtonViaVolumeChange.bind(this));
462        this.videoEl.addEventListener('volumechange', () => {
463            if (this.videoEl.muted) {
464                this.doWhenVideoMute();
465            } else if (this.holdedVolume === 0) {
466                this.doWhenVideoUnmute();
467            }
468        });
469
470        this.videoEl.addEventListener('loadedmetadata', () => {
471            this.totalTime.innerHTML = this.videoDuration(this.videoEl.duration);
472            this.log(`video.width: ${this.videoEl.videoWidth} - video.height: ${this.videoEl.videoHeight}`);
473        });
474
475        this.videoEl.addEventListener('error', this.handleSourceError);
476    }
477
478
479    addDocumentEventListeners() {
480        document.addEventListener('click', (event) => {
481            const isClickedButtonSettingsBtn = this.settingsBtn.c
481ontains(event.target) || event.target === this.settingsBtn;
482            const isClickedAreaOutOfSettingsBox = !this.settingsBox.contains(event.target) && event.target !== this.settingsBox;
483            if (!isClickedButtonSettingsBtn && isClickedAreaOutOfSettingsBox) {
484                this.hide(this.settingsBox);
485            }
486        });
487
488        document.addEventListener('keydown', (event) => this.handleVideoPlayerKeyEvents(event));
489        document.addEventListener('fullscreenchange', () => this.onFullScreenChange());
490
491        this.settingsBox.addEventListener('click', (event) => {
492            event.stopPropagation();
493        });
494    }
495
496    showControlContainer() {
497        this.show(this.controlContainer);
498        this.videoPlayer.classList.add("control-buttons-active");
499        if (this.controllerMouseLeaveListener) {
500            clearTimeout(this.controllerMouseLeaveListener);
501        }
502    }
503
504    hideControlContainer() {
505        if (this.playStatus === enVideoUtils.playStatus.play) {
506            this.controllerMouseLeaveListener = setTimeout(() => {
507                this.hide(this.controlContainer);
508                this.videoPlayer.classList.remove("control-buttons-active");
509            }, 2000);
510        }
511    }
512
513    handleVideoEnded() {
514        this.endVideo();
515    }
516
517    endVideo() {
518        this.hide(this.playBtn, this.pauseBtn);
519        this.show(this.restartBtn);
520        this.setPlayStatus(enVideoUtils.playStatus.end);
521    }
522
523    toggleMuteButtonViaVolumeChange() {
524        this.videoEl.volume = this.volumeEl.value;
525        const volumeValue = Number(this.volumeEl.value);
526        if (volumeValue === 0) {
527            this.mute();
528        } else {
529            this.holdedVolume = volumeValue;
530            this.unmute();
531        }
532    }
533
534    hide(...elements) {
535        enVideoUtils.toggleVisibility(elements, false)
536    }
537
538    show(...elements) {
539        enVideoUtils.toggleVisibility(elements, true);
540    }
541
542    handleRestart() {
543        this.startVideo();
544    }
545
546    handlePlaySpeed(e) {
547        this.videoEl.playbackRate = e.target.value;
548    }
549
550    toggleSettingsMenu(e) {
551        if (this.settingsBox.classList.contains('hidden')) {
552            this.settingsBox.classList.remove('hidden');
553            this.settingsBox.querySelector("select")?.focus(); // try to focus the first child of settings box
554        } else {
555            this.settingsBox.classList.add('hidden');
556        }
557        e.stopPropagation();
558    }
559
560    getFullscreenMethod = (element) => {
561        const method = (
562            element.requestFullscreen ||
563            element.webkitRequestFullscreen ||
564            element.mozRequestFullScreen ||
565            element.msRequestFullscreen
566        );
567        return method;
568    }
569
570    handleFullScreen() {
571        try {
572            const handleFullscreenChange = () => {
573                this.toggleDisplay(this.fullScreenBtn, this.fullScreenExitBtn);
574                this.volumeEl.value = this.videoEl.volume;
575            };
576
577            const fullscreenMethod = this.getFullscreenMethod(this);
578
579            if (fullscreenMethod) {
580                const result = fullscreenMethod.call(this);
581                if (result instanceof Promise) {
582                    result.catch(e => {
583                        console.error("Fullscreen request failed:", e);
584                    });
585                }
586            }
587            // iOS Safari (native fullscreen)
588            else if (enVideoUtils.Browser.Safari === this.currentBrowser) {
589                this.videoPlayer.classList.add("full-screen")
590                handleFullscreenChange();
591
592            } else {
593                console.error("Fullscreen API is not supported in this browser.");
594            }
595        } catch (e) {
596            console.error("Error in handleFullScreen:", e);
597        }
598    }
599
600
601    handleExitFullScreen() {
602        try {
603
604            const handleFullscreenChange = () => {
605                this.toggleDisplay(this.fullScreenExitBtn, this.fullScreenBtn);
606            };
607
608            const exitFullscreenMethod =
609                document.exitFullscreen ||
610                document.webkitExitFullscreen ||
611                document.mozCancelFullScreen ||
612                document.msExitFullscreen;
613
614            if (exitFullscreenMethod) {
615                const result = exitFullscreenMethod.call(document);
616                if (result instanceof Promise) {
617                    result.catch(e => console.error("Error exiting fullscreen:", e));
618                }
619            }
620            // iOS Safari (native fullscreen)
621            else if (enVideoUtils.Browser.Safari === this.currentBrowser) {
622                this.videoPlayer.classList.remove("full-screen");
623                handleFullscreenChange();
624            } else {
625                console.error("Fullscreen exit is not supported in this browser.");
626            }
627        } catch (e) {
628            console.error("Error in handleExitFullScreen:", e);
629        }
630    }
631
632    detectCurrentPlatform = () => {
633        const userAgent = navigator.userAgent || navigator.vendor || window.opera;
634        if (/iPhone|iPad|iPod/i.test(userAgent)) {
635            this.currentPlatform = enVideoUtils.Platform.IOS;
636        }
637        if (/Android/i.test(userAgent)) {
638            this.currentPlatform = enVideoUtils.Platform.Android;
639        }
640        if (/Windows Phone/i.test(userAgent)) {
641            this.currentPlatform = enVideoUtils.Platform.Windows;
642        }
643        this.currentPlatform = enVideoUtils.Platform.Desktop;
644    }
645
646    detectCurrentBrowser = () => {
647        const userAgent = navigator.userAgent;
648        if (/chrome|chromium|crios/i.test(userAgent)) {
649            this.currentBrowser = enVideoUtils.Browser.Chrome;
650        } else if (/safari/i.test(userAgent) && !/chrome|crios/i.test(userAgent)) {
651            this.currentBrowser = enVideoUtils.Browser.Safari;
652        } else if (/firefox|fxios/i.test(userAgent)) {
653            this.currentBrowser = enVideoUtils.Browser.Firefox;
654        } else if (/msie|trident/i.test(userAgent)) {
655            this.currentBrowser = enVideoUtils.Browser.InternetExplorer;
656        } else if (/edg/i.test(userAgent)) {
657            this.currentBrowser = enVideoUtils.Browser.Edge;
658        } else if (/opera|opr/i.test(userAgent)) {
659            this.currentBrowser = enVideoUtils.Browser.Opera;
660        } else {
661            this.currentBrowser = null;
662        }
663    }
664
665
666    handleVideoPlayerKeyEvents(event) {
667        const keyCode = event.code;
668        const key = event.key;
669
670        // event.target hides the inner shadow-root, this returns the actual input/button that was interacted with
671        const enVideoTarget = event.composedPath()?.[0] ?? null;
672
673        if (keyCode === 'Escape' && this.isFullScreen) {
674            this.handleExitFullScreen();
675            return;
676        }
677
678        if (!this.focusedOnMe) {
679            return;
680        }
681
682        switch (keyCode) {
683            case 'KeyF':
684                this.isFullScreen ? this.handleExitFullScreen() : this.handleFullScreen();
685                break;
686            case 'KeyK':
687                this.handlePlayPauseKey();
688                break;
689            case 'Space':
690                // Only play/pause if the user isn't already interacting with something via keyboard
691                if (!event.target || event.target === document.body) {
692                    this.handlePlayPauseKey();
693                    event.preventDefault(); // Prevent scrolling down the page
694                }
695                break;
696            case 'KeyM':
697                this.videoEl.muted ? this.unmute() : this.mute();
698                break;
699            case 'ArrowLeft':
700                // Only skip time if not adjusting volume
701                if (enVideoTarget !== this.volumeEl) {
702                    this.adjustCurrentTimeKey(-10);
703                }
704                break;
705            case 'ArrowRight':
706                // Only skip time if not adjusting volume
707                if (enVideoTarget !== this.volumeEl) {
708                    this.adjustCurrentTimeKey(10);
709                }
710                break;
711            case 'KeyC':
712                this.toggleSubtitle();
713                break;
714            default:
715                if (key === '+' || key === '-') {
716                    this.adjustVolumeKey(key === '+' ? 0.1 : -0.1);
717                }
718                break;
719        }
720    }
721
722    handlePlayPauseKey() {
723        if (this.playStatus === enVideoUtils.playStatus.play) {
724            this.pause();
725        } else if (this.playStatus === enVideoUtils.playStatus.end) {
726            this.startVideo();
727        } else if (this.playStatus === enVideoUtils.playStatus.pause) {
728            this.play();
729        }
730    }
731
732    adjustCurrentTimeKey(seconds) {
733        let currentTime = this.videoEl.currentTime + seconds;
734        currentTime = Math.max(0, Math.min(currentTime, this.videoEl.duration));
735        this.videoEl.currentTime = currentTime;
736    }
737
738    adjustVolumeKey(change) {
739        let volume = parseFloat(this.volumeEl.value) + change;
740        volume = Math.max(0, Math.min(volume, 1));
741        this.videoEl.volume = volume;
742        this.volumeEl.value = volume;
743        this.toggleMuteButtonViaVolumeChange();
744    }
745
746    onFullScreenChange() {
747        this.isFullScreen = document.fullscreenElement;
748        this.log(`Fullscreen change: ${this.isFullScreen}`);
749        if (this.isFullScreen) {
750            this.videoPlayer.classList.add('full-screen');
751            this.toggleDisplay(this.fullScreenBtn, this.fullScreenExitBtn);
752        } else {
753            this.videoPlayer.classList.remove('full-screen');
754            this.toggleDisplay(this.fullScreenExitBtn, this.fullScreenBtn);
755        }
756    }
757
758
759    initPreviewVideo() {
760        this.handleProgressBarMouseMoveDebounced = enVideoUtils.debounce((e) => this.handleProgressBarMouseMove(e), 10);
761        this.handleProgressBarMouseLeaveBound = enVideoUtils.debounce(() => this.handleProgressBarMouseLeave(), 100);
762        this.videoPlayer.classList.add('enable-preview');
763
764        this.previewVideo.addEventListener('loadeddata', () => {
765            this.previewVideoLoaded = true;
766            this.adjustPreviewSize();
767        });
768
769        this.progressBar.addEventListener('mousemove', this.handleProgressBarMouseMoveDebounced);
770        this.progressBar.addEventListener('mouseenter', this.handleProgressBarMouseMoveDebounced);
771        this.progressBar.addEventListener('mouseleave', this.handleProgressBarMouseLeaveBound);
772    }
773
774    adjustPreviewSize() {
775        if (!this.previewVideoLoaded) {
776            return;
777        }
778        const videoWidth = this.previewVideo.videoWidth;
779        const videoHeight = this.previewVideo.videoHeight;
780
781        if (videoWidth === 0 || videoHeight === 0) {
782            return;
783        }
784
785        let maxWidth = 300;
786        let reverseAspectRatio = videoWidth > videoHeight ? this.HORIZONTAL_PREVIEW_HEIGHT_RATIO : this.VERTICAL_PREVIEW_HEIGHT_RATIO;
787        if (enVideoUtils.screenMode.largeMode === this.screenMode) {
788            maxWidth = 450;
789        } else if (enVideoUtils.screenMode.desktopMode === this.screenMode) {
790            maxWidth = 300;
791        } else if (enVideoUtils.screenMode.tabletMode === this.screenMode) {
792            maxWidth = 275;
793        } else if (enVideoUtils.screenMode.smallMode === this.screenMode) {
794            maxWidth = 200;
795        }
796
797        const maxHeight = Number(maxWidth * reverseAspectRatio);
798        const aspectRatio = videoWidth / videoHeight;
799
800        let width = maxHeight * aspectRatio;
801        let height = maxHeight;
802
803        if (width > maxWidth) {
804            width = maxWidth;
805            height = width / aspectRatio;
806        }
807
808        this.previewCanvas.width = width;
809        this.previewCanvas.height = height;
810        this.previewCanvas.style.width = `${width}px`;
811        this.previewCanvas.style.height = `${height}px`;
812
813    }
814
815    handleProgressBarMouseMove(e) {
816        try {
817            if (!this.previewVideo.src && this.previewVideo.querySelectorAll('source').length === 0) {
818                this.log("Preview Video Source is empty.");
819                this.previewContainer.style.display = 'none';
820                return;
821            }
822            if (!this.previewVideoLoaded) {
823                return;
824            }
825            const progressBarRect = this.progressBar.getBoundingClientRect();
826            const progressBarWidth = progressBarRect.width;
827            const previewContainerWidth = this.previewCanvas.width;
828
829            let leftPosition = e.clientX - progressBarRect.left - (previewContainerWidth / 2);
830            if (leftPosition < this.CONTROLLER_PADDING_INLINE) {
831                leftPosition = this.CONTROLLER_PADDING_INLINE;
832            }
833
834            if (leftPosition + previewContainerWidth > progressBarWidth - this.CONTROLLER_PADDING_INLINE) {
835                leftPosition = progressBarWidth - previewContainerWidth;
836            }
837
838
839            const x = e.clientX - progressBarRect.left;
840            const percentage = x / progressBarRect.width;
841            const time = percentage * this.previewVideo.duration;
842            this.previewTimeSpan.innerText = this.videoDuration(time);
843
844            this.previewContainer.style.display = 'inline-block';
845            this.previewContainer.style.left = `${leftPosition}px`;
846
847            if (!this.isSeeking) {
848                this.isSeeking = true;
849                this.previewVideo.currentTime = time;
850                this.previewVideo.addEventListener('seeked', () => {
851                    this.log(`previewCanvas.width: ${this.previewCanvas.width}px;`);
852                    this.previewCanvasContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
853                    this.previewCanvasContext.drawImage(this.previewVideo, 0, 0, this.previewCanvas.width, this.previewCanvas.height);
854                    this.isSeeking = false;
855                }, {once: true});
856            }
857        } catch (e) {
858            this.isSeeking = false;
859            this.previewContainer.style.display = 'none';
860            this.log(`error while preview: ${e}`);
861        }
862    }
863
864    handleProgressBarMouseLeave() {
865        setTimeout(() => {
866            this.previewContainer.style.display = 'none';
867            this.previewCanvasContext.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
868        }, 10);
869    }
870
871    handleProgressBarClick(e) {
872        const rect = this.progressBar.getBoundingClientRect();
873        const x = e.clientX - rect.left;
874        const percentage = x / rect.width;
875        this.videoEl.currentTime = percentage * this.videoEl.duration;
876
877        if (this.playStatus === enVideoUtils.playStatus.end) {
878            this.show(this.playBtn);
879            this.hide(this.restartBtn, this.pauseBtn);
880        }
881    }
882
883    initSubtitles() {
884        try {
885            const tracks = this.querySelectorAll("track");
886            tracks.forEach((track) => {
887                const srcLang = track.getAttribute("srcLang")
888                this.subtitle[srcLang] = {
889                    src: track.src, label: track.label, kind: track.kind,
890                }
891            })
892            this.fetchSubtitle();
893        } catch (e) {
894            console.error(`error while init subtitles ${e}`);
895        }
896    }
897
898    toggleSubtitle() {
899        const isSubtitleActive = this.videoPlayer.classList.contains('subtitle-active');
900        if (this.subTitleBtn.disabled) {
901            return;
902        }
903        if (isSubtitleActive) {
904            this.videoPlayer.classList.remove('subtitle-active');
905            this.subTitleBtn.innerHTML = enVideoUtils.icons.closedSubtitle;
906        } else {
907            this.videoPlayer.classList.add('subtitle-active');
908            this.subTitleBtn.innerHTML = enVideoUtils.icons.subtitle;
909        }
910    }
911
912    setSubtitleMissing() {
913        this.videoPlayer.classList.add('subtitle-missing');
914        this.subTitleBtn.setAttribute("aria-label", "Subtitles/closed captions unavailable keyboard shortcut c");
915        this.subTitleBtn.setAttribute("title", "Subtitles/closed captions unavailable");
916        this.subTitleBtn.disabled = true;
917        this.subTitleBtn.innerHTML = enVideoUtils.icons.missingSubtitle;
918    }
919
920    async fetchSubtitle() {
921        const subtitle = this.subtitle[this.selectedLang];
922
923        if (!subtitle || subtitle[this.selectedLang]?.src.trim().length === 0) {
924            this.setSubtitleMissing();
925            return;
926        }
927
928        try {
929            const response = await fetch(subtitle.src);
930            const data = await response.text();
931            const vtt = this.parseVTT(data);
932
933            if (vtt.length > 0) {
934                subtitle.vtt = vtt;
935                this.trackSubtitle();
936            } else {
937                this.setSubtitleMissing();
938            }
939        } catch (error) {
940            console.error("Couldn't fetch Video Player Subtitle: " + subtitle, error);
941            this.setSubtitleMissing();
942        }
943    }
944
945    trackSubtitle() {
946        const vttLines = this.subtitle[this.selectedLang].vtt;
947
948        if (!vttLines || this.isEmpty(vttLines)) {
949            return;
950        }
951
952        this.videoEl.addEventListener('timeupdate', () => {
953            const currentTime = this.videoEl.currentTime;
954            const line = vttLines.find(line => currentTime >= line.start && currentTime <= line.end)?.text || '';
955            this.subTitleText.innerText = line;
956            // this.log(line);
957        });
958    }
959
960    parseVTT(data) {
961        const lines = data.split('\n');
962        let start, end, text;
963        const vtt = [];
964
965        lines.forEach(line => {
966            if (line.includes('-->')) {
967                const times = line.split(' --> ');
968                start = this.parseTime(times[0]);
969                end = this.parseTime(times[1]);
970            } else if (line.trim() && !line.startsWith('WEBVTT')) {
971                text = line.trim();
972                vtt.push({start, end, text});
973            }
974        });
975        return vtt;
976    }
977
978    parseTime(timeString) {
979        const parts = timeString.split(':');
980        const seconds = parseFloat(parts.pop());
981        const minutes = parseInt(parts.pop());
982        const hours = parseInt(parts.pop()) || 0;
983
984        return hours * 3600 + minutes * 60 + seconds;
985    }
986
987    setVideoMode() {
988        const videoPlayerWidth = this.clientWidth;
989        this.videoPlayer.classList.remove(...Object.values(enVideoUtils.screenMode));
990
991        if (videoPlayerWidth < 576) {
992            this.screenMode = enVideoUtils.screenMode.smallMode;
993        } else if (videoPlayerWidth < 768) {
994            this.screenMode = enVideoUtils.screenMode.tabletMode;
995        } else if (videoPlayerWidth < 1250) {
996            this.screenMode = enVideoUtils.screenMode.desktopMode;
997        } else {
998            this.screenMode = enVideoUtils.screenMode.largeMode;
999        }
1000
1001        this.subTitleText.style.fontSize = "clamp(14px, " + (videoPlayerWidth * 27 / 1200) + "px, 27px)";
1002        this.videoAvailableMessage.style.fontSize = "clamp(14px, " + (videoPlayerWidth * 21 / 1200) + "px, 21px)";
1003        this.videoPlayer.classList.add(this.screenMode);
1004    }
1005
1006    animateMiddleIcon() {
1007        this.middleBox.classList.remove("hidden");
1008        setTimeout(() => {
1009            this.middleBox.classList.add("hidden");
1010        }, 500)
1011    }
1012
1013
1014    initMpdSource() {
1015        try {
1016            if (!this.dataset.mpdSrc) {
1017                return;
1018            }
1019            if (typeof dashjs !== 'undefined' && dashjs !== null) {
1020                this.initDashStreaming()
1021            } else {
1022                const dashJsScriptEle = document.createElement('script');
1023                dashJsScriptEle.src = '/apps-shared/js/dashjs/dash.all.min.js';
1024                dashJsScriptEle.onload = () => {
1025                    if (typeof dashjs !== 'undefined' && dashjs !== null) {
1026                        this.initDashStreaming()
1027                    } else {
1028                        console.error("Failed to load dashjs or dashjs is null");
1029                    }
1030                };
1031                dashJsScriptEle.onerror = () => {
1032                    console.error("Error loading dashjs script");
1033                };
1034                document.head.append(dashJsScriptEle);
1035            }
1036        } catch (e) {
1037            console.error(e);
1038        }
1039    }
1040
1041    initM3U8Source() {
1042        try {
1043            if (enVideoUtils.Browser.Safari === this.currentBrowser && this.dataset.m3u8Src && this.videoEl.canPlayType('application/vnd.apple.mpegurl')) {
1044                this.sourceType = "m3u8";
1045                this.videoEl.src = this.dataset.m3u8Src;
1046                this.previewVideo.src = this.dataset.m3u8Src;
1047                this.videoEl.addEventListener('loadedmetadata', () => {
1048                    if (this.videoEl.muted && this.videoEl.autoplay) {
1049                        this.videoEl.play()
1050                    }
1051                })
1052            }
1053        } catch (e) {
1054            console.error(e);
1055        }
1056    }
1057
1058    initDashStreaming = () => {
1059        if (typeof dashjs === 'undefined' || dashjs === null) {
1060            console.error("dashjs is not loaded or is null");
1061            return;
1062        }
1063
1064        try {
1065            this.sourceType = "mpd";
1066            if (this.mainStreamPlayer) {
1067                this.mainStreamPlayer.reset();
1068            } else {
1069                this.mainStreamPlayer = dashjs.MediaPlayer().create();
1070            }
1071            this.mainStreamPlayer.on(dashjs.MediaPlayer.events.ERROR, (e) => {
1072                console.error(e);
1073                if (this.getSrc()) {
1074                    this.videoEl.src = this.getSrc();
1075                }
1076            });
1077            this.mainStreamPlayer.initialize(this.videoEl, this.dataset.mpdSrc, this.videoEl.autoplay);
1078        } catch (e) {
1079            if (this.getSrc()) {
1080                this.videoEl.src = this.getSrc();
1081            }
1082            console.error(e);
1083        }
1084
1085        try {
1086            if (this.previewStreamPlayer) {
1087                this.previewStreamPlayer.reset();
1088            } else {
1089                this.previewStreamPlayer = dashjs.MediaPlayer().create();
1090            }
1091
1092            this.previewStreamPlayer.on(dashjs.MediaPlayer.events.ERROR, (e) => {
1093                console.error(e);
1094                if (this.getSrc()) {
1095                    this.previewVideo.src = this.getSrc();
1096                }
1097            });
1098
1099            this.previewStreamPlayer.initialize(this.previewVideo, this.dataset.mpdSrc, false);
1100        } catch (e) {
1101            if (this.getSrc()) {
1102                this.previewVideo.src = this.getSrc();
1103            }
1104        }
1105
1106    }
1107
1108    log = (message) => {
1109        if (!this.isDebugMode) {
1110            return;
1111        }
1112        try {
1113            const stack = new Error().stack;
1114            const stackLine = stack.split("\n")[2];
1115            const match = stackLine.match(/\/([^\/]+\.js):(\d+):\d+/);
1116
1117            if (match) {
1118                const fileName = match[1];
1119                const lineNumber = match[2];
1120                console.log(`[${fileName}:${lineNumber}]`, message);
1121            } else {
1122                console.log(message);
1123            }
1124        } catch (e) {
1125            console.log(message);
1126        }
1127    }
1128}
1129
1130
1131customElements.define('en-video', ENVideo);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.