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.