UNPKG

cloudinary-video-player

Version:
233 lines (227 loc) 8.41 kB
const FLUID_CLASS = 'cld-fluid'; /** Same condition as `getPosterUrl` in `poster-url.js` (explicit string `poster`); skips the `cld-poster-url` async chunk. */ const hasExplicitPoster = options => typeof options?.poster === 'string' && options.poster.length > 0; const getVideoElement = elem => { if (typeof elem === 'string') { let id = elem; if (id.indexOf('#') === 0) id = id.slice(1); try { elem = document.querySelector(`#${CSS.escape(id)}`); } catch { elem = null; } if (!elem) throw new Error(`Could not find element with id ${id}`); } if (!elem?.tagName) throw new Error('Must specify either an element or an element id.'); if (elem.tagName !== 'VIDEO') throw new Error('Element is not a video tag.'); return elem; }; const preparePlayerPlaceholder = function (videoElement, posterUrl) { let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; const hadControls = videoElement.hasAttribute('controls'); videoElement.poster = posterUrl; videoElement.preload = 'none'; videoElement.controls = false; videoElement.removeAttribute('controls'); const fluid = options.fluid !== false; if (fluid) { videoElement.classList.add(FLUID_CLASS); } if (options.width) videoElement.setAttribute('width', String(options.width)); if (options.height) videoElement.setAttribute('height', String(options.height)); const ar = options?.sourceOptions?.aspectRatio || options?.aspectRatio; if (typeof ar === 'string' && ar.includes(':')) { const parts = ar.split(':').map(x => parseInt(x.trim(), 10)); if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) { videoElement.style.aspectRatio = `${parts[0]} / ${parts[1]}`; } } return { videoElement, hadControls }; }; const loadPlayer = _ref => { let { overlayRoot, videoElement, options, ready } = _ref; if (overlayRoot?.parentNode) { overlayRoot.replaceWith(videoElement); } return import('./video-player.js').then(function (n) { return n.v; }).then(m => m.createVideoPlayer(videoElement, options, ready)); }; const LAZY_PLAYER_CLASS = 'cld-lazy-player'; const isLightSkin = (videoElement, options) => { const cls = videoElement.className || ''; return cls.indexOf('cld-video-player-skin-light') > -1 || options?.skin === 'light'; }; /** Matches Video.js BigPlayButton DOM structure. */ const createBigPlayButton = () => { const playBtn = document.createElement('button'); playBtn.type = 'button'; playBtn.className = 'vjs-big-play-button'; playBtn.setAttribute('aria-disabled', 'false'); playBtn.title = 'Play Video'; playBtn.setAttribute('aria-label', 'Play Video'); const icon = document.createElement('span'); icon.className = 'vjs-icon-placeholder'; icon.setAttribute('aria-hidden', 'true'); playBtn.appendChild(icon); return playBtn; }; const shouldUseLazyBootstrap = options => !!options?.lazy; const shouldLoadOnScroll = lazy => lazy && typeof lazy === 'object' && lazy.loadOnScroll === true; /** * Renders the lazy placeholder (poster, big-play) before the main player chunk loads. * * @param {string|HTMLVideoElement} elem * @param {object} options * @param {function} [ready] - Passed through when the full player loads. * @returns {Promise<{ source: function, loadPlayer: function }>} */ const lazyBootstrap = async (elem, options, ready) => { const videoElement = getVideoElement(elem); const posterUrl = hasExplicitPoster(options) ? options.poster : (await import(/* webpackChunkName: "cld-poster-url" */'./poster-url.js')).getPosterUrl(options); const loadOnScroll = shouldLoadOnScroll(options.lazy); const fluidEnabled = options?.fluid !== false; const { hadControls } = preparePlayerPlaceholder(videoElement, posterUrl, { fluid: fluidEnabled, width: options?.width, height: options?.height, sourceOptions: options?.sourceOptions, aspectRatio: options?.aspectRatio }); const light = isLightSkin(videoElement, options); const overlayRoot = document.createElement('div'); overlayRoot.classList.add('cld-video-player', 'video-js', LAZY_PLAYER_CLASS); // Fluid rules are `.cld-video-player.cld-fluid`; placeholder `<video>` alone cannot satisfy that selector. if (fluidEnabled) { overlayRoot.classList.add(FLUID_CLASS); } overlayRoot.classList.add(light ? 'cld-video-player-skin-light' : 'cld-video-player-skin-dark'); const colors = options?.colors; if (colors) { if (colors.base) overlayRoot.style.setProperty('--color-base', colors.base); if (colors.accent) overlayRoot.style.setProperty('--color-accent', colors.accent); if (colors.text) overlayRoot.style.setProperty('--color-text', colors.text); } videoElement.parentNode.insertBefore(overlayRoot, videoElement); overlayRoot.appendChild(videoElement); const playBtn = createBigPlayButton(); overlayRoot.appendChild(playBtn); let loadPromise = null; let observer = null; const teardownActivation = () => { playBtn.removeEventListener('click', onPlayClick); videoElement.removeEventListener('click', onVideoClick); if (observer) { observer.disconnect(); observer = null; } }; const activatePlayer = function () { let activationOpts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (loadPromise) { return loadPromise; } teardownActivation(); const autoplayFromUserGesture = activationOpts.autoplayFromUserGesture === true; if (hadControls) videoElement.setAttribute('controls', ''); const wrappedReady = autoplayFromUserGesture ? p => { p.play(); if (ready) ready(p); } : ready; const playerOptions = Object.assign({}, options); delete playerOptions.lazy; loadPromise = loadPlayer({ overlayRoot, videoElement, options: playerOptions, ready: wrappedReady }); return loadPromise; }; function onPlayClick(e) { e.stopPropagation(); activatePlayer({ autoplayFromUserGesture: true }); } function onVideoClick() { activatePlayer({ autoplayFromUserGesture: true }); } playBtn.addEventListener('click', onPlayClick); videoElement.addEventListener('click', onVideoClick); if (loadOnScroll && typeof IntersectionObserver !== 'undefined') { observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { activatePlayer({}); } }); }, { rootMargin: '0px', threshold: 0.25 }); observer.observe(videoElement); } const stub = { source: () => stub, loadPlayer: () => activatePlayer({ autoplayFromUserGesture: true }) }; return stub; }; const createAsyncPlayer = async (id, playerOptions, ready, createFn) => { const mergedOptions = Object.assign({}, playerOptions); const videoElement = getVideoElement(id); const opts = await (async () => { try { const { fetchAndMergeConfig } = await import('./fetch-config.js'); const fetched = await fetchAndMergeConfig(mergedOptions); return Object.assign({}, fetched, mergedOptions); } catch { return mergedOptions; } })(); if (opts?.schedule?.weekly) { const { shouldUseScheduleBootstrap, scheduleBootstrap } = await import('./schedule.js'); if (shouldUseScheduleBootstrap(opts)) { return scheduleBootstrap(id, opts, ready); } } if (shouldUseLazyBootstrap(opts)) { return lazyBootstrap(id, opts, ready); } return createFn(videoElement, opts, ready); }; const createMultiplePlayers = async (selector, playerOptions, ready, playerFn) => { const nodeList = document.querySelectorAll(selector); return Promise.all([...nodeList].map(node => playerFn(node, playerOptions, ready))); }; const createMultipleSync = (selector, playerOptions, ready, playerFn) => { const nodeList = document.querySelectorAll(selector); return [...nodeList].map(node => playerFn(node, playerOptions, ready)); }; const setupCloudinaryGlobal = methods => { const cloudinary = { ...(window.cloudinary || {}), ...methods }; window.cloudinary = cloudinary; return cloudinary; }; export { createMultiplePlayers as a, createAsyncPlayer as b, createMultipleSync as c, getVideoElement as g, loadPlayer as l, preparePlayerPlaceholder as p, setupCloudinaryGlobal as s };