UNPKG

@atmtfy/video-background

Version:

Automatic background video from various sources (Youtube, MP4, vimeo) with autoplay across devices. No JS dependencies.

258 lines (215 loc) 6.58 kB
import Logger from "../utils/logger"; import type { VideoBackground } from "videoBackground"; const defaults: DefaultShape = { status: { ready: false, error: false, paused: true, playing: false, started: false, muted: true, apiReady: false, intersecting: false, }, props: { aspectRatio: 0.5625, muted: true, poster: false, autoplay: true, }, config : { loop: true, zoom: 1.3, can: {unmute: false, pause: false}, startTime: 0, fillMode: 'fill', threshold: 0.2, }, } interface ListenersShape { [key:string] : number } export default class VideoPlayer { passedConfig: PlayerConfigInput config: PlayerConfigShape status: PlayerStatusShape props: PlayerPropsShape observer?: IntersectionObserver logger: Logger type: PlayerType callbacks: PlayerCallbacks iframe?: HTMLIFrameElement wrapper?: HTMLElement parent: VideoBackground parseParams():void { if(this.parent.hasAttribute('zoom')) { this.config.zoom = parseFloat(this.parent.getAttribute('zoom')!) } if(this.parent.hasAttribute('can-unmute')) { this.config.can.pause = this.parent.getAttribute('can-unmute') != "false" } if(this.parent.hasAttribute('loop')) { this.config.can.pause = this.parent.getAttribute('loop') != "false" } if(this.parent.hasAttribute('can-pause')) { this.config.can.pause = this.parent.getAttribute('can-pause') != "false" } if(this.parent.hasAttribute('fill-mode')) { this.config.fillMode = this.parent.getAttribute('fill-mode') == 'fit' ? 'fit' : 'fill' } if(this.parent.hasAttribute('start')) { this.config.startTime = parseInt(this.parent.getAttribute('start')!) } } hasRequiredParams(config:PlayerConfigShape):boolean { if (config.parent == null) { console.log("Cannot initialize player: No parent provided to Player via constructor") return false; } if (config.source == null ) { console.log("Cannot initialize player: No source provided to Player via constructor") return false; } return true; } constructor(config: PlayerConfigInput) { this.status = defaults.status; this.props = defaults.props; this.type = "local"; this.passedConfig = config; this.callbacks ={'ready': [{fn: this.playCheck.bind(this)}], 'intersecting':[{fn:this.playCheck.bind(this)}] } this.config = this.mergeConfig(config); this.logger = this.config.parent.logger; this.parent = this.config.parent; this.parseParams(); if (!this.hasRequiredParams(this.config)){ this.status.error = true; throw new Error("No parent passed, cannot create player"); } this.init(); } build() { //Defined these in individual functions } unbuild() { //Defined these in individual functions } init() { // this.build(); window.addEventListener('resize', this.resize.bind(this)) this.buildIntersectionObserver() } destroy() { this.unbuild(); this.removeIntersectionObserver(); window.removeEventListener('resize', this.resize.bind(this)) } mergeConfig(config:PlayerConfigInput):PlayerConfigShape { return Object.assign(defaults.config, config); } do(eventName:string) { const callbacks = this.callbacks[eventName]; if (typeof callbacks == 'object') { callbacks.forEach(c=> { return c.fn() }) } } on(eventName:string, callback:()=>void, uniqueKey: string | undefined = undefined) { const arrayExists = typeof this.callbacks[eventName] == 'object' if (arrayExists && uniqueKey) { const callbackExists = this.callbacks[eventName].find(c=>c.key == uniqueKey) if (!callbackExists) { this.callbacks[eventName].push({fn:callback, key: uniqueKey}) } } else { if (arrayExists) { this.callbacks[eventName].push({fn:callback}) } else { this.callbacks[eventName] = [{fn:callback}] } } } playCheck():void { const { ready, intersecting } = this.status; if (ready) { if ( intersecting) { this.play() } else { this.pause() } } } async pause():Promise<boolean> { return true; } async play():Promise<boolean> { return true; } async mute():Promise<void> { return ; } async unmute():Promise<void> { return ; } resize() { if (this.type != 'local') { //Common for iframes let iframe:HTMLElement | undefined | null = null; if (this.wrapper) { iframe = this.wrapper.querySelector('iframe'); } else { iframe = this.iframe; } if (!iframe) { console.log('No iframe found.') return; } iframe.removeAttribute('width') iframe.removeAttribute('height') if (this.config.fillMode == 'fit') { iframe.style.removeProperty('top') iframe.style.removeProperty('left') iframe.style.removeProperty('width') iframe.style.removeProperty('height') return; } const container = {w: this.parent.clientWidth, h: this.parent.clientHeight, ratio: this.parent.clientHeight / this.parent.clientWidth } let final = {w:container.w * this.config.zoom, h: container.h* this.config.zoom} const {aspectRatio }= this.props const { zoom }= this.config if (container.ratio < aspectRatio) { final.w = container.w * zoom final.h = final.w / aspectRatio } else if (container.ratio > aspectRatio) { final.h = container.h * zoom final.w = final.h / aspectRatio } else { final.h = container.h final.w = container.w } iframe.style.width = final.w + 'px' iframe.style.height = final.h + 'px' iframe.style.left = 0 - ((final.w - container.w) / 2) + 'px' iframe.style.top = 0 - ((final.h - container.h) / 2) + 'px' } } buildIntersectionObserver() { const options = { threshold: this.config.threshold } this.observer = new IntersectionObserver(this.handleIntersection.bind(this), options); this.observer.observe(this.parent); } removeIntersectionObserver() { this.observer?.disconnect(); } handleIntersection(entries:IntersectionObserverEntry[],observer:IntersectionObserver) { entries.forEach((entry:IntersectionObserverEntry)=>{ if ( entry.target == this.parent) { this.status.intersecting = entry.isIntersecting; this.do('intersecting') } }) // this.do('intersect') } }