import { AtomBinder } from "@web-atoms/core/dist/core/AtomBinder"; import Bind from "@web-atoms/core/dist/core/Bind"; import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty"; import XNode from "@web-atoms/core/dist/core/XNode"; import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl"; import { ChildEnumerator } from "@web-atoms/core/dist/web/core/AtomUI"; // check if it is a mobile.. const isTouchEnabled = /android|iPhone|iPad/i.test(navigator.userAgent); import "./AtomVideoPlayer.global.css"; const gatherElements = (e: HTMLElement, data = {}) => { const ce = ChildEnumerator.enumerate(e); for (const iterator of ce) { const elementName = iterator.dataset.element?.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); if (elementName) { data[elementName] = iterator; } gatherElements(iterator, data); } return data; }; const numberToText = (n: number) => { if (n < 10) { return "0" + n; } return n.toString(); }; const durationText = (n: number, total: number) => { if (n === null || n === undefined) { return ""; } const minutes = Math.floor(n / 60); const seconds = numberToText(Math.ceil(n % 60)); const totalMinutes = Math.floor(total / 60); const totalSeconds = numberToText(Math.ceil(total % 60)); return `${minutes}:${seconds} / ${totalMinutes}:${totalSeconds}`; }; const noSoundIcon = "fa-duotone fa-volume-slash"; const mute = "fa-duotone fa-volume-xmark"; const low = "fa-duotone fa-volume-low"; const mid = "fa-duotone fa-volume"; const high = "fa-duotone fa-volume-high"; export type playerState = "playing" | "paused" | "ended" | "waiting" | "aborted" | "none"; const getPlayIcon = (state: playerState) => { switch(state) { case "ended": return "fa-solid fa-refresh"; case "paused": return "fa-solid fa-play"; case "playing": return "fa-solid fa-pause"; } return "fa-solid fa-play"; }; export default class AtomVideoPlayer extends AtomControl { @BindableProperty public source: any; @BindableProperty public logo: any; @BindableProperty public logoTitle: string; @BindableProperty public logoDescription: string; public get poster() { return this.video.poster; } public set poster(v: string) { this.video.poster = v; } /** * Use this inside a mobile app */ public useStageView: boolean; public get state() { return this.element.getAttribute("data-state") as playerState; } public set state(v: playerState) { this.element.setAttribute("data-state", v); AtomBinder.refreshValue(this, "paused"); AtomBinder.refreshValue(this, "state"); } public get duration() { return this.video.duration; } public get time() { return this.video.currentTime; } public set time(v) { this.video.currentTime = v; } public get paused() { return this.video.paused; } public get isFullScreen() { return (document.fullscreenEnabled && this.element === document.fullscreenElement) ?? false; } private video: HTMLVideoElement; private progress: HTMLCanvasElement; private currentTimeSpan: HTMLSpanElement; private soundIcon: HTMLElement; private volumeRange: HTMLInputElement; private maxWidth: string = ""; public stopFullscreen() { if(this.isFullScreen) { return document.exitFullscreen(); } return Promise.resolve(); } public pause() { this.video.pause(); } public play() { // tslint:disable-next-line: no-console this.video.play().catch(console.error); } public onPropertyChanged(name: keyof AtomVideoPlayer): void { switch (name) { case "source": this.updateSource(); break; } } protected setCurrentTime(n: number) { // n = Math.round(n * 100) / 100; this.video.currentTime = n * this.video.duration; } protected create(): void { this.element.dataset.videoPlayer = "video-player"; this.bindEvent(this.element, "togglePlay", (e: CustomEvent) => { if (e.defaultPrevented) { return; } e.preventDefault(); if (isTouchEnabled) { if (this.state === "playing") { if (e.target === this.video) { if (this.element.dataset.controls === "true") { this.element.dataset.controls = "false"; } else { this.element.dataset.controls = "true"; } return; } } if ((e.target as HTMLCanvasElement).tagName === "CANVAS") { return; } // if (e.target === e.currentTarget) { // if (this.element.dataset.controls === "true") { // this.element.dataset.controls = "false"; // } else { // this.element.dataset.controls = "true"; // } // return; // } } if (this.video.paused) { this.video.play(); this.element.dataset.controls = "false"; } else { this.video.pause(); this.element.dataset.controls = "true"; } }); this.bindEvent(this.element, "volume", (e: CustomEvent) => { this.video.muted = !this.video.muted; this.updateVolume(); }); this.bindEvent(this.element, "fullScreen", async (e: CustomEvent) => { if (!this.element.requestFullscreen) { (this.video as any).webkitEnterFullscreen(); return; } if (this.isFullScreen) { await document.exitFullscreen(); return; } await this.element.requestFullscreen({navigationUI: "show" }); }); this.bindEvent(document as any, "fullscreenchange", () => { AtomBinder.refreshValue(this, "isFullScreen"); }); this.render(
this.logo ? "true" : "false", "false")} data-element="banner" style-width={Bind.oneWay(() => this.maxWidth)}> this.logo)}/>
this.logo && this.logoTitle)}/>
this.logo && this.logoDescription)}/>
); const all = gatherElements(this.element) as any; this.video = all.video; this.progress = all.progress; this.currentTimeSpan = all.current; this.soundIcon = all.sound; this.volumeRange = all.volumeRange; if (this.volumeRange) { this.bindEvent(this.volumeRange, "input", () => { setTimeout(() => { this.video.volume = parseFloat(this.volumeRange.value); }, 1); }); } this.bindEvent(this.element, "mouseenter", () => { this.element.dataset.controls = "true"; }); this.bindEvent(this.element, "mouseleave", () => { this.element.dataset.controls = "false"; }); this.bindEvent(this.progress, "pointerdown", (e: PointerEvent) => { e.preventDefault(); // const scale = this.progress.clientWidth / this.video.duration ; this.setCurrentTime(e.offsetX / this.progress.clientWidth); const move = (e1: PointerEvent) => { e1.preventDefault(); this.setCurrentTime(e1.offsetX / this.progress.clientWidth); }; const up = (e1: PointerEvent) => { e1.preventDefault(); this.progress.removeEventListener("pointermove", move); this.progress.removeEventListener("pointerup", up); }; this.progress.addEventListener("pointermove", move); this.progress.addEventListener("pointerup", up); }); } protected updateSource() { this.video.src = this.source; } private updateProgress() { const context = this.progress.getContext("2d"); // context.fillStyle = "rgba(0,0,0,0)"; context.strokeStyle = "rgba(0,0,0,0)"; const width = this.progress.clientWidth; const height = this.progress.clientHeight; this.progress.width = width; this.progress.height = height; context.clearRect(0, 0, width, height); const max = this.video.duration; const seekable = this.video.buffered; const scale = width / max; context.fillStyle = "rgba(255,255,255,0.3)"; context.fillRect(0, 0, width, height); context.fillStyle = "rgba(255,255,255,0.6)"; for (let index = 0; index < seekable.length; index++) { const start = seekable.start(index) * scale; const end = seekable.end(index) * scale; context.fillRect(start, 0, end, height); } context.fillStyle = "#ffffff"; context.fillRect(0, 0, this.video.currentTime * scale, height); } private updateVolume() { if (this.video.muted) { this.soundIcon.className = mute; this.soundIcon.title = "Unmute"; if (this.volumeRange) { this.volumeRange.style.display = "none"; } return; } const audio = this.video.volume; if (this.volumeRange) { this.volumeRange.style.display = ""; this.volumeRange.value = audio?.toString(); } this.soundIcon.title = "Mute"; if (audio > 0.8) { this.soundIcon.className = high; return; } if (audio < 0.2) { this.soundIcon.className = low; return; } this.soundIcon.className = mid; } }