UNPKG

fluid-player

Version:

Fluid Player is a free HTML5 video player

394 lines (326 loc) 15.4 kB
import { displayModes } from '../constants/constants'; export default function (playerInstance) { // Module constants const MINIMUM_WIDTH = 400; // Pixels const MINIMUM_HEIGHT = 225; // Pixels const MINIMUM_WIDTH_MOBILE = 40; // Percentage const TOGGLE_BY_VISIBILITY_DETECTION_RATE = 1000 / 60; // ms const DISABLE_MINI_PLAYER_MOBILE_ANIMATION_CLAMP = 50; const DISABLE_MINI_PLAYER_MOBILE_ANIMATION_DEADZONE = 5; const DESKTOP_ONLY_MEDIA_QUERY = '(max-width: 768px)'; const FLUID_PLAYER_WRAPPER_CLASS = 'fluid_mini_player_mode'; const CLOSE_BUTTON_WRAPPER_CLASS = 'mini-player-close-button-wrapper'; const CLOSE_BUTTON_CLASS = 'mini-player-close-button'; const PLACEHOLDER_CLASS = 'fluidplayer-miniplayer-player-placeholder' const DISABLE_MINI_PLAYER_MOBILE_CLASS = 'disable-mini-player-mobile'; const LINEAR_CLICKTHROUGH_SELECTOR = '.vast_clickthrough_layer'; const NON_LINEAR_SELECTOR = '.fluid_nonLinear_ad img, .fluid_vpaid_nonlinear_slot_iframe'; const VPAID_FRAME_SELECTOR = '.fluid_vpaidNonLinear_frame'; const MINI_PLAYER_TOGGLE_EVENT = 'miniPlayerToggle'; // Module variables let originalWidth = null; let originalHeight = null; let originalNonLinearWidth = null let originalNonLinearHeight = null; let isSetup = false; /** @type null | Element */ let placeholderElement = null; let isMobile = false; /** @type boolean */ let toggleByVisibilityControl = false; /** * Toggles the MiniPlayer given that it's enabled. Resets all other display modes. * * @param {'on'|'off'} [forceToggle] * @param {boolean} manualToggle * @param toAnotherDisplayTarget */ function toggleMiniPlayer(forceToggle, manualToggle = false, toAnotherDisplayTarget = false) { playerInstance.debugMessage(`[MiniPlayer] Toggling MiniPlayer, forceToggle: ${forceToggle}`); const miniPlayerOptions = playerInstance.displayOptions.layoutControls.miniPlayer; if (!miniPlayerOptions.enabled) { playerInstance.debugMessage(`[MiniPlayer] Prevent toggle MiniPlayer, it's currently disabled`); return; } if ((forceToggle === 'on' && playerInstance.miniPlayerToggledOn) || (forceToggle === 'off' && !playerInstance.miniPlayerToggledOn)) { playerInstance.debugMessage(`[MiniPlayer] Can't force toggle Mini Player to it's same state`); return; } if (manualToggle) { toggleScreenDetection(); } if (window.matchMedia(DESKTOP_ONLY_MEDIA_QUERY).matches) { isMobile = true; } const previousDisplayMode = playerInstance.getPreviousDisplayMode(); // Important as the player can be in full screen or theater mode playerInstance.resetDisplayMode(displayModes.MINI_PLAYER); if (!isSetup) { // Setups JIT to avoid extra processing setupMiniPlayer(); } if (forceToggle === 'off' || playerInstance.miniPlayerToggledOn) { toggleMiniPlayerOff(); } else if (forceToggle === 'on' || !playerInstance.miniPlayerToggledOn) { toggleMiniPlayerOn(miniPlayerOptions.width, miniPlayerOptions.height, miniPlayerOptions.widthMobile, miniPlayerOptions.position); } if (!toAnotherDisplayTarget) { playerInstance.trackPlayerSizeChanged(previousDisplayMode); } } /** * Setups custom Mini Player DOM */ function setupMiniPlayer() { const hasCloseButton = Boolean(playerInstance.domRef.player.parentNode.querySelector(`.${CLOSE_BUTTON_CLASS}`)); if (!hasCloseButton) { const closeButtonWrapper = document.createElement('div'); closeButtonWrapper.classList.add(CLOSE_BUTTON_WRAPPER_CLASS); const closeButton = document.createElement('span'); closeButton.classList.add(CLOSE_BUTTON_CLASS); closeButton.addEventListener('click', () => { toggleMiniPlayer('off', true); if (!playerInstance.domRef.player.paused) { playerInstance.playPauseToggle(); } }); closeButtonWrapper.appendChild(closeButton); playerInstance.domRef.player.parentNode.append(closeButtonWrapper); } if (isMobile) { setupMobile(); } isSetup = true; } /** * Toggles the MiniPlayer off and restores previous functionality to player */ function toggleMiniPlayerOff() { const videoWrapper = playerInstance.domRef.wrapper; removePlayerPlaceholder(); videoWrapper.classList.remove(FLUID_PLAYER_WRAPPER_CLASS); videoWrapper.style.width = `${originalWidth}px`; videoWrapper.style.height = `${originalHeight}px`; originalWidth = null; originalHeight = null; adaptNonLinearSize(); adaptLinearSize(); playerInstance.miniPlayerToggledOn = false; emitToggleEvent(); } /** * Toggles the MiniPlayer on, stores the original size of the player. * * @param {number} width * @param {number} height * @param {number} mobileWidth * @param {'top left'|'top right'|'bottom left'|'bottom right'} position */ function toggleMiniPlayerOn(width, height, mobileWidth, position) { const videoWrapper = playerInstance.domRef.wrapper; const targetWidth = width > MINIMUM_WIDTH ? width : MINIMUM_WIDTH; const targetHeight = height > MINIMUM_HEIGHT ? height : MINIMUM_HEIGHT; const targetMobileWidth = mobileWidth > MINIMUM_WIDTH_MOBILE ? mobileWidth : MINIMUM_WIDTH_MOBILE; originalWidth = extractSizeFromElement(videoWrapper, 'width', 'clientWidth'); originalHeight = extractSizeFromElement(videoWrapper, 'height', 'clientHeight'); videoWrapper.classList.add( FLUID_PLAYER_WRAPPER_CLASS, `${FLUID_PLAYER_WRAPPER_CLASS}--${position.trim().replace(/\s/, '-')}` ); if (!isMobile) { videoWrapper.style.width = `${targetWidth}px`; videoWrapper.style.height = `${targetHeight}px`; } else { videoWrapper.style.width = `${targetMobileWidth}vw`; videoWrapper.style.height = `auto`; videoWrapper.style.aspectRatio = `16 / 9`; } createPlayerPlaceholder(originalWidth, originalHeight); adaptNonLinearSize(targetWidth, targetHeight, targetMobileWidth); adaptLinearSize(); playerInstance.miniPlayerToggledOn = true; emitToggleEvent(); } /** * Emits event to Fluid Player Event API */ function emitToggleEvent() { playerInstance.domRef.player.dispatchEvent( new CustomEvent(MINI_PLAYER_TOGGLE_EVENT, { detail: { isToggledOn: playerInstance.miniPlayerToggledOn } }) ); } /** * Extracts size from an element checking multiple element properties * * @param {HTMLElement} element * @param {'width'|'height'|null} styleProperty * @param {'clientWidth'|'clientHeight'|'width'|'height'} htmlProperty * @returns {number} */ function extractSizeFromElement(element, styleProperty, htmlProperty) { if (styleProperty && element.style[styleProperty] && element.style[styleProperty].match('px')) { return parseInt(element.style[styleProperty]); } else { return String(element[htmlProperty]).match('px') ? parseInt(element[htmlProperty]) : element[htmlProperty]; } } /** * Adapts NonLinear size (if present) to fit MiniPlayer view * * @param {number} [width] * @param {number} [height] * @param {number} [mobileWidth] */ function adaptNonLinearSize(width, height, mobileWidth) { /** @type HTMLImageElement|HTMLIFrameElement */ const nonLinear = playerInstance.domRef.wrapper.querySelector(NON_LINEAR_SELECTOR); /** @type HTMLElement */ const vpaidFrame = playerInstance.domRef.wrapper.querySelector(VPAID_FRAME_SELECTOR); if (!nonLinear) return; if (isMobile) { width = window.innerWidth * mobileWidth / 100; // Transforms vw to px } const nonLinearWidth = extractSizeFromElement(nonLinear, null, 'width'); const nonLinearHeight = extractSizeFromElement(nonLinear, null, 'height'); if (originalNonLinearWidth && originalNonLinearHeight) { nonLinear.width = originalNonLinearWidth; nonLinear.height = originalNonLinearHeight; if (vpaidFrame) { vpaidFrame.style.width = `${originalNonLinearWidth}px`; vpaidFrame.style.height = `${originalNonLinearHeight}px`; } originalNonLinearWidth = originalNonLinearHeight = null; } else if (nonLinearWidth > width || nonLinearHeight > height) { const targetRatio = (width - (isMobile ? 4 : 32)) / nonLinearWidth; originalNonLinearWidth = nonLinearWidth; originalNonLinearHeight = nonLinearHeight; nonLinear.width = Math.round(nonLinearWidth * targetRatio); nonLinear.height = Math.round(nonLinearHeight * targetRatio); if (vpaidFrame) { vpaidFrame.style.width = `${Math.round(nonLinearWidth * targetRatio)}px`; vpaidFrame.style.height = `${Math.round(nonLinearHeight * targetRatio)}px`; } } } /** * Adapts Linear size (if present) to fit MiniPlayer view */ function adaptLinearSize() { const clickTroughLayer = playerInstance.domRef.wrapper.querySelector(LINEAR_CLICKTHROUGH_SELECTOR); if (clickTroughLayer) { clickTroughLayer.style.width = `${playerInstance.domRef.player.offsetWidth}px`; clickTroughLayer.style.height = `${playerInstance.domRef.player.offsetHeight}px`; } } /** * Setups mobile disable element */ function setupMobile() { const disableMiniPlayerMobile = document.createElement('div'); let animationAmount = 0; let startTimestamp = 0; let startScreenX = 0; let hasTriggeredAnimation; disableMiniPlayerMobile.classList.add(DISABLE_MINI_PLAYER_MOBILE_CLASS); const closeButton = document.createElement('span'); closeButton.classList.add(CLOSE_BUTTON_CLASS); disableMiniPlayerMobile.appendChild(closeButton); disableMiniPlayerMobile.ontouchstart = event => { hasTriggeredAnimation = false; startTimestamp = event.timeStamp; startScreenX = event.changedTouches[0].screenX; event.preventDefault(); } disableMiniPlayerMobile.ontouchmove = event => { animationAmount = Math.min( Math.max( startScreenX - event.changedTouches[0].screenX, DISABLE_MINI_PLAYER_MOBILE_ANIMATION_CLAMP * -1), DISABLE_MINI_PLAYER_MOBILE_ANIMATION_CLAMP ); if (Math.abs(animationAmount) > DISABLE_MINI_PLAYER_MOBILE_ANIMATION_DEADZONE) { // Moves the element the same amount as the touch event moved playerInstance.domRef.wrapper.style.transform = `translateX(${animationAmount * -1}px)`; hasTriggeredAnimation = true; } else { playerInstance.domRef.wrapper.style.transform = `translateX(0px)` } } disableMiniPlayerMobile.ontouchend = event => { if (Math.abs(animationAmount) > DISABLE_MINI_PLAYER_MOBILE_ANIMATION_DEADZONE) { // Scroll X behaviour - Disable mini player and pauses video toggleMiniPlayer('off', true); if (!playerInstance.domRef.player.paused) { playerInstance.playPauseToggle(); } event.preventDefault(); } else if (!hasTriggeredAnimation) { // Tap behaviour - Disable mini player and moves screen to video toggleMiniPlayer('off', true); setTimeout(() => { playerInstance.domRef.wrapper.scrollIntoView({ behavior: 'smooth', block: 'center', }); }) } animationAmount = 0; playerInstance.domRef.wrapper.style.transform = ``; } // Fallback for when there is no touch event disableMiniPlayerMobile.onmouseup = () => toggleMiniPlayer('off', true) playerInstance.domRef.wrapper.insertBefore(disableMiniPlayerMobile, playerInstance.domRef.player.nextSibling); } /** * Creates a placeholder element in place where the video player was * * @param {number} placeholderWidth * @param {number} placeholderHeight */ function createPlayerPlaceholder(placeholderWidth, placeholderHeight) { placeholderElement = document.createElement('div'); placeholderElement.classList.add(PLACEHOLDER_CLASS); placeholderElement.style.height = `${placeholderHeight}px`; placeholderElement.style.width = `${placeholderWidth}px`; placeholderElement.innerText = playerInstance.displayOptions.layoutControls.miniPlayer.placeholderText || ''; placeholderElement.onclick = () => toggleMiniPlayer('off', true); playerInstance.domRef.wrapper.parentElement.insertBefore(placeholderElement, playerInstance.domRef.wrapper); } /** * Removes the placeholder that was in place where video player was */ function removePlayerPlaceholder() { playerInstance.domRef.wrapper.parentElement.removeChild(placeholderElement); placeholderElement = null; } /** * Toggles auto toggle for mini player */ function toggleScreenDetection() { const autoToggle = playerInstance.displayOptions.layoutControls.miniPlayer.autoToggle; if (toggleByVisibilityControl || !autoToggle) { document.removeEventListener('scroll', toggleMiniPlayerByVisibility); return; } toggleByVisibilityControl = true; document.addEventListener('scroll', toggleMiniPlayerByVisibility, { passive: true }); } /** * Checks for player visibility and toggles mini player based on it */ const toggleMiniPlayerByVisibility = playerInstance.throttle(function toggleMiniPlayerByVisibility() { if (playerInstance.domRef.player.paused) { return; } const isPlayerVisible = playerInstance.isElementVisible(playerInstance.domRef.player); const isPlaceholderVisible = playerInstance.isElementVisible(playerInstance.domRef.wrapper.querySelector(`.${PLACEHOLDER_CLASS}`)); if (!isPlayerVisible && !playerInstance.miniPlayerToggledOn) { toggleMiniPlayer('on'); } else if (isPlaceholderVisible && playerInstance.miniPlayerToggledOn) { toggleMiniPlayer('off'); } }, TOGGLE_BY_VISIBILITY_DETECTION_RATE); // Exposes public module functions playerInstance.toggleMiniPlayer = toggleMiniPlayer; playerInstance.toggleMiniPlayerScreenDetection = toggleScreenDetection; }