cloudinary-video-player
Version:
Cloudinary Video Player
233 lines (227 loc) • 8.41 kB
JavaScript
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 };