UNPKG

@atmtfy/video-background

Version:

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

385 lines (315 loc) 9.44 kB
import { checkForAutoplay } from "./utils/vidUtils"; import { compileSources } from "./utils/sources"; import { initializeVimeoAPI, initializeVimeoPlayer } from './utils/vimeo'; import { initializeYouTubeAPI, initializeYouTubePlayer } from './utils/youtube'; import Icons from './utils/icons'; import Logger from './utils/logger'; import { LocalPlayer } from "./players/localPlayer"; import { VimeoPlayer } from "./players/vimeoPlayer"; import { YoutubePlayer } from "./players/youtubePlayer"; var is_ios = /iP(ad|od|hone)/i.test(window.navigator.userAgent), is_safari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/); /** * Object for choosing the correct video initializer. */ const videoSourceModules = { vimeo: { api: initializeVimeoAPI, player: initializeVimeoPlayer }, youtube: { api: initializeYouTubeAPI, player: initializeYouTubePlayer } } interface CanAutoPlayShape { video: boolean, audio: boolean } /** * @class VideoBackground creates a custom web component * @todo separate statuses and capabilities and stores into their own objects * @todo allow unmuting */ export class VideoBackground extends HTMLElement { initialized?: boolean breakpoints?:number[] /** Contains an array of breakpoints to reload smaller sources when passed */ container : HTMLElement can: VideoCan canAutoplay?:CanAutoPlayShape muteButton?:HTMLElement overlayEl?:HTMLElement pauseButton?:HTMLElement player?: YoutubePlayer | VimeoPlayer | LocalPlayer icons?: Icons playerReadyTimeout?: NodeJS.Timeout paused: boolean size?: string muted: boolean src: string | undefined | null posterEl?:HTMLImageElement|HTMLPictureElement startTime?:number sourceId?:string sources?: SourcesShape type?: 'local' | 'youtube' | 'vimeo' | 'error' url?:string videoEl?:HTMLVideoElement logger: Logger constructor() { super(); //Setting up props //Get this started ASAP this.initialized = false this.container = this; this.src = this.getAttribute('src'); this.can = { unmute: this.hasAttribute('can-unmute'), pause: this.hasAttribute('can-pause')}; this.muted = this.getAttribute('muted') !== 'false'; this.logger = new Logger(this.getAttribute('debug')); if (is_ios && is_safari) { this.muted = true; } this.paused = false; // this.init(); } init() { /*Check if we need to re-init */ if (this.initialized != true) { this.initSync(); this.buildDOM(); } else { this.reset(); this.init(); } } initSync() { // this.logger.log('Initializing video background') this.status = "loading"; //Compile sources if (this.src == null) { return; } const compiled = compileSources(this.src); if (compiled) { this.type = compiled.type; this.sources = compiled.sources; this.url = compiled.url; if (compiled.breakpoints) { this.breakpoints = compiled.breakpoints } } } afterAutoplay() { if (!this.canAutoplay) { throw new Error("Should never run before autoplay support is defined") } if (!this.canAutoplay.audio) { this.can.unmute = false } if (this.canAutoplay.video) { this.buildVideo(); this.buildIcons(); } else { this.logger.log("Can't play video: Autoplay is not supported", true) this.handleFallbackNoVideo(); } this.initialized = true } async buildDOM() { this.buildOverlay(); this.buildPoster(); if (this.canAutoplay) { this.afterAutoplay() } checkForAutoplay().then((autoplay:CanAutoPlayShape)=> { this.canAutoplay = autoplay; this.afterAutoplay() }).catch(e => { console.error(e); this.handleFallbackNoVideo(); }) } buildIcons() { if (this.can.unmute || this.can.pause) { this.icons = new Icons({wrapper: this,can: this.can, onMuteUnmute: this.toggleMute.bind(this), onPausePlay: this.togglePause.bind(this) , initialState: { muted: this.player?.muted ?? true, paused: false}}) } } async buildVideo() { //Never should have mixed sources. if (!this.sources || !this.type) { this.initSync(); // return this.handleFallbackNoVideo(); } if (!this.sources || !this.type) { return this.handleFallbackNoVideo(); } // this.logger.log(`Building ${this.type} video based on source: ${this.sources[0].url}` ); if (this.type == 'local' ) { this.player = new LocalPlayer({source: this.sources!, parent: this, breakpoints: this.breakpoints}) // this.buildLocalVideo() //Check to make sure we have sources } else if (this.type == 'vimeo') { this.player = new VimeoPlayer({source: this.sources, parent: this}) } else if (this.type == 'youtube') { this.player = new YoutubePlayer({source: this.sources, parent: this}) } } handleFallbackNoVideo() { this.status = "fallback"; this.logger.log("Video Won't play, defaulting to fallback") this.status = "fallback"; } toggleMute() { if (this.muted == true) { this.unmuteVideo() this.muted = false; } else { this.muteVideo(); this.muted = true; } } togglePause() { if (this.player) { if (this.player.paused) { this.player.play() } else { this.player.pause() } } else { this.logger.log("No video to pause/play") } } muteVideo() { if (this.player) { this.player.mute() } else { this.logger.log('No player to mute') } } unmuteVideo() { if (this.player) { this.logger.log('unmuting video'); this.player.unmute() } else { this.logger.log('No player to unmute') } } checkForInherentPoster():HTMLImageElement|HTMLPictureElement|false { const inherentPoster = this.container.querySelector('img') ? this.container.querySelector('img') : this.container.querySelector('picture') if (inherentPoster) { return inherentPoster; } return false; } buildPoster() { let hasInherentPoster = false //Gets a poster image element that's a child of the video-background element const inherentPoster = this.checkForInherentPoster(); if (!this.posterSet && !this.poster && !inherentPoster) { return false; } if (inherentPoster != false) { this.logger.log("Found an inherent poster"); //Found a poster element hasInherentPoster = true; this.posterEl = inherentPoster; this.container.innerHTML = ''; } else { this.container.innerHTML = ''; //Create a poster element if none found. this.posterEl = document.createElement('img'); this.posterEl.classList.add('vbg--loading') if (this.poster && 'src' in this.posterEl) { const self = this; const imageLoaderEl = new Image(); imageLoaderEl.src = this.poster; imageLoaderEl.addEventListener('load', function() { if (self && self.posterEl && 'src' in self.posterEl) { self.posterEl.src = imageLoaderEl.src; self.posterEl.classList.remove('vbg--loading') } }) } if (this.posterSet && 'srcset' in this.posterEl) { this.posterEl.srcset = this.posterSet; this.posterEl.sizes = this.size ?? "100vw"; } } //Add styling classes; this.posterEl.classList.add('vbg__poster') this.appendChild(this.posterEl); } buildOverlay() { this.overlayEl = document.createElement('div'); this.overlayEl.classList.add('vbg__overlay'); this.appendChild(this.overlayEl); } get status():loadingStatus { const statusString = this.getAttribute('status'); if (typeof statusString == 'string' && (statusString == "loading" || statusString == "none" || statusString == "ready" || statusString == "fallback")) { return statusString; } else { this.status = "none"; return "none" } } /** Updates status on the actual element as well as the property of the class */ set status(status) { if (!status) { this.status = "fallback" } } get poster():string|false{ const posterVal = this.getAttribute('poster'); if (posterVal != null) { return posterVal; } else { return false; } } get posterSet():string|false{ const posterVal = this.getAttribute('posterset'); if (posterVal != null) { return posterVal; } else { return false; } } /** * Sets the poster url string, and sets loading that poster into motion */ set poster(posterString) { } // handleMalformedSource(url:string):Source { this.logger.log(`Handling error for ${url}`) return { url: url, type: 'error', } } reset() { if (this.initialized) { this.logger.log("Resetting video-background."); //Setting up props this.initialized = false; this.container = this; this.status = "none"; this.muted = this.getAttribute('muted') !== 'false'; if (is_ios && is_safari) { this.muted = true; } this.paused = false; } } attributeChangedCallback() { this.reset(); this.init(); } connectedCallback() { this.init() } disconnectedCallback() { this.reset() } }