UNPKG

fluid-player

Version:

Fluid Player is a free HTML5 video player

1,280 lines (1,075 loc) 137 kB
'use strict'; import { displayModes } from './constants/constants'; // Player modules import VPAIDModule from './modules/vpaid'; import VASTModule from './modules/vast'; import CardboardModule from './modules/cardboard'; import SubtitleModule from './modules/subtitles'; import TimelineModule from './modules/timeline'; import AdSupportModule from './modules/adsupport'; import StreamingModule from './modules/streaming'; import UtilsModule from './modules/utils'; import SuggestedVideosModule from './modules/suggestedVideos'; import MiniPlayerModule from './modules/miniplayer'; const FP_MODULES = [ VPAIDModule, VASTModule, CardboardModule, SubtitleModule, TimelineModule, AdSupportModule, StreamingModule, UtilsModule, SuggestedVideosModule, MiniPlayerModule ]; // Determine build mode // noinspection JSUnresolvedVariable const FP_DEVELOPMENT_MODE = typeof FP_ENV !== 'undefined' && FP_ENV === 'development'; // Are we running in debug mode? // noinspection JSUnresolvedVariable const FP_RUNTIME_DEBUG = typeof FP_DEBUG !== 'undefined' && FP_DEBUG === true; let playerInstances = 0; const fluidPlayerClass = function () { // "self" always points to current instance of the player within the scope of the instance // This should help readability and context awareness slightly... const self = this; self.domRef = { player: null }; // noinspection JSUnresolvedVariable self.version = typeof FP_BUILD_VERSION !== 'undefined' ? FP_BUILD_VERSION : ''; // noinspection JSUnresolvedVariable self.homepage = typeof FP_HOMEPAGE !== 'undefined' ? FP_HOMEPAGE + '/?utm_source=player&utm_medium=context_menu&utm_campaign=organic' : ''; self.destructors = []; self.init = (playerTarget, options) => { // Install player modules and features const moduleOptions = { development: FP_DEVELOPMENT_MODE, debug: FP_RUNTIME_DEBUG, }; for (const playerModule of FP_MODULES) { playerModule(self, moduleOptions); } let playerNode; if (playerTarget instanceof HTMLVideoElement) { playerNode = playerTarget; // Automatically assign ID if none exists if (!playerTarget.id) { playerTarget.id = 'fluid_player_instance_' + (playerInstances++).toString(); } } else if (typeof playerTarget === 'string' || playerTarget instanceof String) { playerNode = document.getElementById(playerTarget); } else { throw 'Invalid initializer - player target must be HTMLVideoElement or ID'; } if (!playerNode) { throw 'Could not find a HTML node to attach to for target ' + playerTarget + '"'; } if (playerNode.classList.contains('js-fluid-player')) { throw 'Invalid initializer - player target already is initialized'; } playerNode.setAttribute('playsinline', ''); playerNode.setAttribute('webkit-playsinline', ''); playerNode.classList.add('js-fluid-player'); self.domRef.player = playerNode; self.vrROTATION_POSITION = 0.1; self.vrROTATION_SPEED = 80; self.vrMode = false; self.vrPanorama = null; self.vrViewer = null; self.vpaidTimer = null; self.vpaidAdUnit = null; self.vastOptions = null; /** * Don't use this as a way to change the DOM. DOM manipulation should be done with domRef. */ self.videoPlayerId = playerNode.id; self.originalSrc = self.getCurrentSrc(); self.isCurrentlyPlayingAd = false; self.isCurrentlyShowingNonLinearAd = false; self.recentWaiting = false; self.latestVolume = 1; self.currentVideoDuration = 0; self.firstPlayLaunched = false; self.suppressClickthrough = false; self.timelinePreviewData = []; self.mainVideoCurrentTime = 0; self.mainVideoDuration = 0; self.isTimer = false; self.timer = null; self.timerPool = {}; self.rollsById = {}; self.adPool = {}; self.adGroupedByRolls = {}; self.onPauseRollAdPods = []; self.currentOnPauseRollAd = ''; self.preRollAdsResolved = false; self.preRollAdPods = []; self.preRollAdPodsLength = 0; self.preRollVastResolved = 0; self.temporaryAdPods = []; self.availableRolls = ['preRoll', 'midRoll', 'postRoll', 'onPauseRoll']; self.supportedNonLinearAd = ['300x250', '468x60', '728x90']; self.autoplayAfterAd = true; self.nonLinearDuration = 15; self.supportedStaticTypes = ['image/gif', 'image/jpeg', 'image/png']; self.inactivityTimeout = null; self.isUserActive = null; self.nonLinearVerticalAlign = 'bottom'; self.vpaidNonLinearCloseButton = true; self.showTimeOnHover = true; self.initialAnimationSet = true; self.theatreMode = false; self.theatreModeAdvanced = false; self.fullscreenMode = false; self.originalWidth = playerNode.offsetWidth; self.originalHeight = playerNode.offsetHeight; self.dashPlayer = false; self.hlsPlayer = false; self.dashScriptLoaded = false; self.hlsScriptLoaded = false; self.isPlayingMedia = false; self.isSwitchingSource = false; self.isLoading = false; self.isInIframe = self.inIframe(); self.mainVideoReadyState = false; self.xmlCollection = []; self.inLineFound = null; self.fluidStorage = {}; self.fluidPseudoPause = false; self.mobileInfo = self.getMobileOs(); self.events = {}; self.timeSkipOffsetAmount = 10; // Only for linear ads, non linear are not taken into account self.currentMediaSourceType = 'source'; self.isLiveStream = null; self.previousTime = 0; //Default options self.displayOptions = { layoutControls: { mediaType: self.getCurrentSrcType(), primaryColor: false, posterImage: false, posterImageSize: 'contain', adProgressColor: '#f9d300', playButtonShowing: true, playPauseAnimation: true, closeButtonCaption: 'Close', // Remove? fillToContainer: false, autoPlay: false, preload: 'auto', mute: false, loop: null, keyboardControl: true, allowDownload: false, playbackRateEnabled: false, subtitlesEnabled: false, subtitlesOnByDefault: true, showCardBoardView: false, showCardBoardJoystick: false, allowTheatre: true, doubleclickFullscreen: true, autoRotateFullScreen: false, theatreSettings: { width: '100%', height: '60%', marginTop: 0, horizontalAlign: 'center', keepPosition: false }, theatreAdvanced: { theatreElement: null, }, title: null, logo: { imageUrl: null, position: 'top left', clickUrl: null, opacity: 1, mouseOverImageUrl: null, imageMargin: '2px', hideWithControls: false, showOverAds: false }, controlBar: { autoHide: false, autoHideTimeout: 3, animated: true, playbackRates: ['x2', 'x1.5', 'x1', 'x0.5'] }, timelinePreview: { spriteImage: false, spriteRelativePath: false }, htmlOnPauseBlock: { html: null, height: null, width: null }, layout: 'default', //options: 'default', '<custom>' playerInitCallback: (function () { }), persistentSettings: { volume: true, quality: true, speed: true, theatre: true }, controlForwardBackward: { show: false, doubleTapMobile: true }, contextMenu: { controls: true, links: [] }, miniPlayer: { enabled: true, width: 400, height: 225, widthMobile: 50, placeholderText: 'Playing in Miniplayer', position: 'bottom right', autoToggle: false, }, roundedCorners: 0 }, suggestedVideos: { configUrl: null }, vastOptions: { adList: {}, skipButtonCaption: 'Skip ad in [seconds]', skipButtonClickCaption: 'Skip Ad <span class="skip_button_icon"></span>', adText: null, adTextPosition: 'top left', adCTAText: 'Visit now!', adCTATextPosition: 'bottom right', adCTATextVast: false, adClickable: true, vastTimeout: 5000, showProgressbarMarkers: false, allowVPAID: false, showPlayButton: false, maxAllowedVastTagRedirects: 3, vpaidTimeout: 3000, vastAdvanced: { vastLoadedCallback: (function () { }), noVastVideoCallback: (function () { }), vastVideoSkippedCallback: (function () { }), vastVideoEndedCallback: (function () { }) } }, hls: { overrideNative: false }, captions: { play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', fullscreen: 'Fullscreen', subtitles: 'Subtitles', exitFullscreen: 'Exit Fullscreen', }, debug: FP_RUNTIME_DEBUG, modules: { configureHls: (options) => { return options; }, onBeforeInitHls: (hls) => { }, onAfterInitHls: (hls) => { }, configureDash: (options) => { return options; }, onBeforeInitDash: (dash) => { }, onAfterInitDash: (dash) => { } }, onBeforeXMLHttpRequestOpen: (request) => { }, onBeforeXMLHttpRequest: (request) => { if (FP_RUNTIME_DEBUG || FP_DEVELOPMENT_MODE) { console.debug('[FP_DEBUG] Request made', request); } } }; if (!!options.hlsjsConfig) { console.error('[FP_ERROR] player option hlsjsConfig is removed and has no effect. ' + 'Use module callbacks instead!') } /** * Replaces values from objects without replacing the default object * * @param defaults * @param options * @returns {object} */ function overrideDefaults(defaults, options) { Object.keys(options).forEach(defaultKey => { if ( typeof options[defaultKey] === 'object' && options[defaultKey] !== null && !Array.isArray(options[defaultKey]) ) { overrideDefaults(defaults[defaultKey], options[defaultKey]); } else if (typeof options[defaultKey] !== 'undefined') { defaults[defaultKey] = options[defaultKey]; } }); return defaults; } overrideDefaults(self.displayOptions, options); self.domRef.wrapper = self.setupPlayerWrapper(); playerNode.addEventListener('webkitfullscreenchange', self.recalculateAdDimensions); playerNode.addEventListener('fullscreenchange', self.recalculateAdDimensions); playerNode.addEventListener('waiting', self.onRecentWaiting); playerNode.addEventListener('pause', self.onFluidPlayerPause); playerNode.addEventListener('error', self.onErrorDetection); playerNode.addEventListener('ended', self.onMainVideoEnded); playerNode.addEventListener('durationchange', () => { self.currentVideoDuration = self.getCurrentVideoDuration(); }); playerNode.addEventListener('seeked', self.onVideoSeeked); // 'loadedmetadata' inconsistently fires because the audio can already be loaded when the listener is added. // Here we use readystate to see if metadata has already loaded if (playerNode.readyState > 0) { self.mainVideoReady(); } else { playerNode.addEventListener('loadedmetadata', self.mainVideoReady); } if (self.displayOptions.layoutControls.showCardBoardView) { // This fixes cross origin errors on three.js playerNode.setAttribute('crossOrigin', 'anonymous'); } //Manually load the video duration if the video was loaded before adding the event listener self.currentVideoDuration = self.getCurrentVideoDuration(); if (isNaN(self.currentVideoDuration) || !isFinite(self.currentVideoDuration)) { self.currentVideoDuration = 0; } self.setLayout(); //Set the volume control state self.latestVolume = playerNode.volume; // Set the default animation setting self.initialAnimationSet = self.displayOptions.layoutControls.playPauseAnimation; //Set the custom fullscreen behaviour self.handleFullscreen(); self.initLogo(); self.initTitle(); self.initMute(); self.initLoop(); self.displayOptions.layoutControls.playerInitCallback(); self.createVideoSourceSwitch(); self.createSubtitles(); self.createCardboard(); self.userActivityChecker(); self.setVastList(); self.setPersistentSettings(); self.generateSuggestedVideoList(); // Previously prevented to be initialized if preRolls were set up // but now the streamers support reinitialization self.initialiseStreamers(); const _play_videoPlayer = playerNode.play; playerNode.play = function () { let promise = null; if (self.displayOptions.layoutControls.showCardBoardView) { if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { DeviceOrientationEvent.requestPermission() .then(function (response) { if (response === 'granted') { self.debugMessage('DeviceOrientationEvent permission granted!'); } }) .catch(console.error); } } try { promise = _play_videoPlayer.apply(this, arguments); if (promise !== undefined && promise !== null) { promise.then(() => { self.isPlayingMedia = true; clearTimeout(self.promiseTimeout); }).catch(error => { console.error('[FP_ERROR] Playback error', error); const isAbortError = (typeof error.name !== 'undefined' && error.name === 'AbortError'); // Ignore abort errors which caused for example Safari or autoplay functions // (example: interrupted by a new load request) // (example: interrupted by a new load request) if (isAbortError) { // Ignore AbortError error reporting } else { self.announceLocalError(202, 'Failed to play video.'); } clearTimeout(self.promiseTimeout); }); self.promiseTimeout = setTimeout(function () { if (self.isPlayingMedia === false) { self.announceLocalError(204, '[FP_ERROR] Timeout error. Failed to play video?'); } }, 5000); } return promise; } catch (error) { console.error('[FP_ERROR] Playback error', error); self.announceLocalError(201, 'Failed to play video.'); } }; const videoPauseOriginal = playerNode.pause; playerNode.pause = function () { if (self.isPlayingMedia === true) { self.isPlayingMedia = false; return videoPauseOriginal.apply(this, arguments); } // just in case if (self.isCurrentlyPlayingVideo(self.domRef.player)) { try { self.isPlayingMedia = false; return videoPauseOriginal.apply(this, arguments); } catch (e) { self.announceLocalError(203, 'Failed to play video.'); } } }; if (!!self.displayOptions.layoutControls.autoPlay && !self.dashScriptLoaded && !self.hlsScriptLoaded) { //There is known issue with Safari 11+, will prevent autoPlay, so we wont try const browserVersion = self.getBrowserVersion(); if ('Safari' === browserVersion.browserName) { return; } playerNode.play(); } if (!self.mobileInfo.userOs) { if (!self.displayOptions.layoutControls.controlBar.autoHide) { self.domRef.wrapper.addEventListener('mouseleave', self.handleMouseleave, false); } self.domRef.wrapper.addEventListener('mouseenter', self.showControlBar, false); self.domRef.wrapper.addEventListener('mouseenter', self.showTitle, false); } else { //On mobile mouseleave behavior does not make sense, so it's better to keep controls, once the playback starts //Autohide behavior on timer is a separate functionality self.hideControlBar(); self.domRef.wrapper.addEventListener('touchstart', self.showControlBar, { passive: true }); } //Keyboard Controls if (self.displayOptions.layoutControls.keyboardControl) { self.keyboardControl(); } if (self.displayOptions.layoutControls.controlBar.autoHide) { self.linkControlBarUserActivity(); } // Hide the captions on init if user added subtitles track. // We are taking captions track kind of as metadata try { if (!!self.domRef.player.textTracks) { for (const textTrack of self.domRef.player.textTracks) { textTrack.mode = 'hidden'; } } } catch (_ignored) { } }; self.getCurrentVideoDuration = () => { if (self.domRef.player) { return self.domRef.player.duration; } return 0; }; self.toggleLoader = (showLoader) => { self.isLoading = !!showLoader; const loaderDiv = self.domRef.wrapper.querySelector('.vast_video_loading'); if (loaderDiv) { loaderDiv.style.display = showLoader ? 'table' : 'none'; } }; self.sendRequest = (url, withCredentials, timeout, functionReadyStateChange) => { const xmlHttpReq = new XMLHttpRequest(); xmlHttpReq.onreadystatechange = functionReadyStateChange; self.displayOptions.onBeforeXMLHttpRequestOpen(xmlHttpReq); xmlHttpReq.open('GET', url, true); xmlHttpReq.withCredentials = withCredentials; xmlHttpReq.timeout = timeout; self.displayOptions.onBeforeXMLHttpRequest(xmlHttpReq); xmlHttpReq.send(); }; /** * Makes a XMLHttpRequest encapsulated by a Promise * * @param url * @param withCredentials * @param timeout * @returns {Promise<unknown>} */ self.sendRequestAsync = async (url, withCredentials, timeout) => { return await new Promise((resolve, reject) => { const xmlHttpReq = new XMLHttpRequest(); xmlHttpReq.onreadystatechange = (event) => { const response = event.target; if (response.readyState === 4 && response.status >= 200 && response.status < 300) { resolve(response); } else if (response.readyState === 4) { reject(response); } }; self.displayOptions.onBeforeXMLHttpRequestOpen(xmlHttpReq); xmlHttpReq.open('GET', url, true); xmlHttpReq.withCredentials = withCredentials; xmlHttpReq.timeout = timeout; self.displayOptions.onBeforeXMLHttpRequest(xmlHttpReq); xmlHttpReq.send(); }) }; // TODO: rename self.announceLocalError = (code, msg) => { const parsedCode = typeof (code) !== 'undefined' ? parseInt(code) : 900; let message = '[Error] (' + parsedCode + '): '; message += !msg ? 'Failed to load Vast' : msg; console.warn(message); }; // TODO: move this somewhere else and refactor self.debugMessage = (...msg) => { const style = 'color: #fff; font-weight: bold; background-color: #1a5e87; padding: 3px 6px; border-radius: 3px;'; if (self.displayOptions.debug) { console.log('%cFP DEBUG', style, ...msg); } }; self.onMainVideoEnded = (event) => { self.debugMessage('onMainVideoEnded is called'); if (self.isCurrentlyPlayingAd && self.autoplayAfterAd) { // It may be in-stream ending, and if it's not postroll then we don't execute anything return; } //we can remove timer as no more ad will be shown if (Math.floor(self.getCurrentTime()) >= Math.floor(self.mainVideoDuration)) { // play pre-roll ad // sometime pre-roll ad will be missed because we are clearing the timer self.adKeytimePlay(Math.floor(self.mainVideoDuration)); clearInterval(self.timer); } if (!!self.displayOptions.layoutControls.loop) { self.switchToMainVideo(); self.playPauseToggle(); } // Event listener doesn't wait on flags to be flipped from post roll ads, needs small time out to compensate setTimeout(() => { if (!self.isCurrentlyPlayingAd && self.displayOptions.suggestedVideos.configUrl) { self.displaySuggestedVideos(); } }, 100); }; self.getCurrentTime = () => { return self.isCurrentlyPlayingAd ? self.mainVideoCurrentTime : self.domRef.player.currentTime; }; /** * Gets the src value of the first source element of the video tag. * * @returns string|null */ self.getCurrentSrc = () => { const sources = self.domRef.player.getElementsByTagName('source'); if (sources.length) { return sources[0].getAttribute('src'); } return null; }; /** * Src types required for streaming elements */ self.getCurrentSrcType = () => { const sources = self.domRef.player.getElementsByTagName('source'); if (!sources.length) { return null; } for (let i = 0; i < sources.length; i++) { if (sources[i].getAttribute('src') === self.originalSrc) { return sources[i].getAttribute('type').toLowerCase(); } } return null; }; self.onRecentWaiting = () => { self.recentWaiting = true; setTimeout(function () { self.recentWaiting = false; }, 1000); }; /** * Dispatches a custom pause event which is not present when seeking. */ self.onFluidPlayerPause = () => { setTimeout(function () { if (self.recentWaiting) { return; } const event = document.createEvent('CustomEvent'); event.initEvent('fluidplayerpause', false, true); self.domRef.player.dispatchEvent(event); }, 100); }; self.checkShouldDisplayVolumeBar = () => { return 'iOS' !== self.getMobileOs().userOs; }; self.generateCustomControlTags = (options) => { const controls = {}; // Loader controls.loader = document.createElement('div'); controls.loader.className = 'vast_video_loading'; controls.loader.style.display = 'none'; // Root element controls.root = document.createElement('div'); controls.root.className = 'fluid_controls_container'; if (!options.displayVolumeBar) { controls.root.className = controls.root.className + ' no_volume_bar'; } if (options.controlForwardBackward) { controls.root.className = controls.root.className + ' skip_controls'; } // Left container controls.leftContainer = document.createElement('div'); controls.leftContainer.className = 'fluid_controls_left'; controls.root.appendChild(controls.leftContainer); // Left container -> Play/Pause controls.playPause = document.createElement('div'); controls.playPause.className = 'fluid_button fluid_button_play fluid_control_playpause'; controls.leftContainer.appendChild(controls.playPause); if (options.controlForwardBackward) { // Left container -> Skip backwards controls.skipBack = document.createElement('div'); controls.skipBack.className = 'fluid_button fluid_button_skip_back'; controls.leftContainer.appendChild(controls.skipBack); // Left container -> Skip forward controls.skipForward = document.createElement('div'); controls.skipForward.className = 'fluid_button fluid_button_skip_forward'; controls.leftContainer.appendChild(controls.skipForward); } // Progress container controls.progressContainer = document.createElement('div'); controls.progressContainer.className = 'fluid_controls_progress_container fluid_slider'; controls.root.appendChild(controls.progressContainer); // Progress container -> Progress wrapper controls.progressWrapper = document.createElement('div'); controls.progressWrapper.className = 'fluid_controls_progress'; controls.progressContainer.appendChild(controls.progressWrapper); // Progress container -> Progress wrapper -> Current progress controls.progressCurrent = document.createElement('div'); controls.progressCurrent.className = 'fluid_controls_currentprogress'; controls.progressCurrent.style.backgroundColor = options.primaryColor; controls.progressWrapper.appendChild(controls.progressCurrent); // Progress container -> Progress wrapper -> Current progress -> Marker controls.progress_current_marker = document.createElement('div'); controls.progress_current_marker.className = 'fluid_controls_currentpos'; controls.progressCurrent.appendChild(controls.progress_current_marker); // Progress container -> Buffered indicator controls.bufferedIndicator = document.createElement('div'); controls.bufferedIndicator.className = 'fluid_controls_buffered'; controls.progressContainer.appendChild(controls.bufferedIndicator); // Progress container -> Ad markers controls.adMarkers = document.createElement('div'); controls.adMarkers.className = 'fluid_controls_ad_markers_holder'; controls.progressContainer.appendChild(controls.adMarkers); // Right container controls.rightContainer = document.createElement('div'); controls.rightContainer.className = 'fluid_controls_right'; controls.root.appendChild(controls.rightContainer); // Right container -> Fullscreen controls.fullscreen = document.createElement('div'); controls.fullscreen.className = 'fluid_button fluid_control_fullscreen fluid_button_fullscreen'; controls.fullscreen.title = 'Full Screen'; controls.rightContainer.appendChild(controls.fullscreen); if (options.miniPlayer.enabled) { // Right container -> MiniPlayer controls.miniPlayer = document.createElement('div'); controls.miniPlayer.className = 'fluid_button fluid_control_mini_player fluid_button_mini_player'; controls.miniPlayer.title = 'Mini Player'; controls.rightContainer.appendChild(controls.miniPlayer); } // Right container -> Theatre controls.theatre = document.createElement('div'); controls.theatre.className = 'fluid_button fluid_control_theatre fluid_button_theatre'; controls.theatre.title = 'Theatre Mode'; controls.rightContainer.appendChild(controls.theatre); // Right container -> Cardboard controls.cardboard = document.createElement('div'); controls.cardboard.className = 'fluid_button fluid_control_cardboard fluid_button_cardboard'; controls.cardboard.title = 'Cardboard'; controls.rightContainer.appendChild(controls.cardboard); // Right container -> Subtitles controls.subtitles = document.createElement('div'); controls.subtitles.className = 'fluid_button fluid_control_subtitles fluid_button_subtitles'; controls.subtitles.title = 'Captions'; controls.rightContainer.appendChild(controls.subtitles); // Right container -> Video source controls.videoSource = document.createElement('div'); controls.videoSource.className = 'fluid_button fluid_control_video_source fluid_button_video_source'; controls.videoSource.title = 'Source'; controls.rightContainer.appendChild(controls.videoSource); // Right container -> Playback rate controls.playbackRate = document.createElement('div'); controls.playbackRate.className = 'fluid_button fluid_control_playback_rate fluid_button_playback_rate'; controls.playbackRate.title = 'Playback Rate'; controls.rightContainer.appendChild(controls.playbackRate); // Right container -> Download controls.download = document.createElement('div'); controls.download.className = 'fluid_button fluid_control_download fluid_button_download'; controls.download.title = 'Download'; controls.rightContainer.appendChild(controls.download); // Right container -> Volume container controls.volumeContainer = document.createElement('div'); controls.volumeContainer.className = 'fluid_control_volume_container fluid_slider'; controls.rightContainer.appendChild(controls.volumeContainer); // Right container -> Volume container -> Volume controls.volume = document.createElement('div'); controls.volume.className = 'fluid_control_volume'; controls.volumeContainer.appendChild(controls.volume); // Right container -> Volume container -> Volume -> Current controls.volumeCurrent = document.createElement('div'); controls.volumeCurrent.className = 'fluid_control_currentvolume'; controls.volume.appendChild(controls.volumeCurrent); // Right container -> Volume container -> Volume -> Current -> position controls.volumeCurrentPos = document.createElement('div'); controls.volumeCurrentPos.className = 'fluid_control_volume_currentpos'; controls.volumeCurrent.appendChild(controls.volumeCurrentPos); // Right container -> Volume container controls.mute = document.createElement('div'); controls.mute.className = 'fluid_button fluid_button_volume fluid_control_mute'; controls.rightContainer.appendChild(controls.mute); // Right container -> Volume Control + Live Steam Button const durationContainer = document.createElement('div'); durationContainer.className = 'fluid_control_duration'; controls.duration = document.createElement('div'); controls.duration.className = 'fluid_fluid_control_duration'; controls.duration.innerText = '00:00 / 00:00'; if (!options.displayVolumeBar) { durationContainer.className = durationContainer.className + ' no_volume_bar'; } controls.live_indicator = document.createElement('div'); controls.live_indicator.className = 'fluid_control_live_indicator'; durationContainer.append(controls.live_indicator, controls.duration); controls.rightContainer.appendChild(durationContainer); return controls; }; self.showLiveIndicator = () => { const liveIndicatorButton = self.domRef.player.parentNode.getElementsByClassName('fluid_button_live_indicator'); if (!liveIndicatorButton.length) { const liveIndicator = self.domRef.player.parentNode.getElementsByClassName('fluid_control_live_indicator'); const liveIndicatorButton = document.createElement('span'); liveIndicatorButton.className = 'fluid_button_live_indicator'; liveIndicatorButton.innerHTML = `LIVE<span class="live_circle"></span>`; liveIndicatorButton.addEventListener('click', () => { self.domRef.player.currentTime = self.currentVideoDuration; }); for (let i = 0; i < liveIndicator.length; i++) { liveIndicator[i].appendChild(liveIndicatorButton); } } }; self.hideLiveIndicator = () => { const liveIndicatorButton = self.domRef.player.parentNode.getElementsByClassName('fluid_button_live_indicator')[0]; if (liveIndicatorButton) { liveIndicatorButton.remove(); } }; self.controlPlayPauseToggle = () => { const playPauseButton = self.domRef.player.parentNode.getElementsByClassName('fluid_control_playpause'); const menuOptionPlay = self.domRef.wrapper.querySelector('.context_option_play'); const controlsDisplay = self.domRef.player.parentNode.getElementsByClassName('fluid_controls_container'); const fpLogo = self.domRef.wrapper.querySelector('.logo_holder'); const initialPlay = self.domRef.wrapper.querySelector('.fluid_initial_play'); if (initialPlay) { self.domRef.wrapper.querySelector('.fluid_initial_play').style.display = "none"; self.domRef.wrapper.querySelector('.fluid_initial_play_button_container').style.opacity = "1"; } if (!self.domRef.player.paused) { for (let i = 0; i < playPauseButton.length; i++) { playPauseButton[i].className = playPauseButton[i].className.replace(/\bfluid_button_play\b/g, 'fluid_button_pause'); } for (let i = 0; i < controlsDisplay.length; i++) { controlsDisplay[i].classList.remove('initial_controls_show'); } if (fpLogo) { fpLogo.classList.remove('initial_controls_show'); } if (menuOptionPlay !== null) { menuOptionPlay.innerHTML = self.displayOptions.captions.pause; } return; } for (let i = 0; i < playPauseButton.length; i++) { playPauseButton[i].className = playPauseButton[i].className.replace(/\bfluid_button_pause\b/g, 'fluid_button_play'); } for (let i = 0; i < controlsDisplay.length; i++) { controlsDisplay[i].classList.add('initial_controls_show'); } if (self.isCurrentlyPlayingAd && self.displayOptions.vastOptions.showPlayButton) { self.domRef.wrapper.querySelector('.fluid_initial_play').style.display = "block"; self.domRef.wrapper.querySelector('.fluid_initial_play_button_container').style.opacity = "1"; } if (fpLogo) { fpLogo.classList.add('initial_controls_show'); } if (menuOptionPlay !== null) { menuOptionPlay.innerHTML = self.displayOptions.captions.play; } }; self.playPauseAnimationToggle = (play) => { if (self.isCurrentlyPlayingAd || !self.displayOptions.layoutControls.playPauseAnimation || self.isSwitchingSource) { return; } const playButtonElement = self.domRef.wrapper.querySelector('.fluid_initial_play_button, .fluid_initial_pause_button'); if (play) { playButtonElement.classList.remove('fluid_initial_pause_button'); playButtonElement.classList.add('fluid_initial_play_button'); } else { playButtonElement.classList.remove('fluid_initial_play_button'); playButtonElement.classList.add('fluid_initial_pause_button'); } self.domRef.wrapper.querySelector('.fluid_initial_play').classList.add('transform-active'); setTimeout( function () { self.domRef.wrapper.querySelector('.fluid_initial_play').classList.remove('transform-active'); }, 800 ); }; self.controlProgressbarUpdate = () => { const currentProgressTag = self.domRef.player.parentNode.getElementsByClassName('fluid_controls_currentprogress'); for (let i = 0; i < currentProgressTag.length; i++) { currentProgressTag[i].style.width = (self.domRef.player.currentTime / self.currentVideoDuration * 100) + '%'; } }; self.controlDurationUpdate = () => { const currentPlayTime = self.formatTime(self.domRef.player.currentTime); let isLiveHls = false; if (self.hlsPlayer) { isLiveHls = self.hlsPlayer.levels && self.hlsPlayer.levels[self.hlsPlayer.currentLevel] && self.hlsPlayer.levels[self.hlsPlayer.currentLevel].details.live; } let durationText; if (isNaN(self.currentVideoDuration) || !isFinite(self.currentVideoDuration) || isLiveHls) { durationText = currentPlayTime; } else { const totalTime = self.formatTime(self.currentVideoDuration); durationText = currentPlayTime + ' / ' + totalTime; } const timePlaceholder = self.domRef.player.parentNode.getElementsByClassName('fluid_control_duration'); for (let i = 0; i < timePlaceholder.length; i++) { timePlaceholder[i].innerHTML = ''; if (self.isLiveStream) { const liveIndicatorButton = document.createElement('span'); liveIndicatorButton.className = 'fluid_button_live_indicator'; liveIndicatorButton.innerHTML = `LIVE<span class="live_circle"></span>`; liveIndicatorButton.addEventListener('pointerdown', () => { self.domRef.player.currentTime = self.currentVideoDuration; }); timePlaceholder[i].appendChild(liveIndicatorButton); } const durationTextElement = document.createElement('span'); durationTextElement.className = 'fluid_fluid_control_duration'; durationTextElement.innerText = durationText; timePlaceholder[i].appendChild(durationTextElement); } }; self.controlVolumebarUpdate = () => { const currentVolumeTag = self.domRef.wrapper.querySelector('.fluid_control_currentvolume'); const volumeposTag = self.domRef.wrapper.querySelector('.fluid_control_volume_currentpos'); const volumebarTotalWidth = self.domRef.wrapper.querySelector('.fluid_control_volume').clientWidth; const volumeposTagWidth = volumeposTag.clientWidth; const muteButtonTag = self.domRef.player.parentNode.getElementsByClassName('fluid_control_mute'); const menuOptionMute = self.domRef.wrapper.querySelector('.context_option_mute'); if (0 !== self.domRef.player.volume) { self.latestVolume = self.domRef.player.volume; self.fluidStorage.fluidMute = false; } else { self.fluidStorage.fluidMute = true; } if (self.domRef.player.volume && !self.domRef.player.muted) { for (let i = 0; i < muteButtonTag.length; i++) { muteButtonTag[i].className = muteButtonTag[i].className.replace(/\bfluid_button_mute\b/g, 'fluid_button_volume'); } if (menuOptionMute !== null) { menuOptionMute.innerHTML = self.displayOptions.captions.mute; } } else { for (let i = 0; i < muteButtonTag.length; i++) { muteButtonTag[i].className = muteButtonTag[i].className.replace(/\bfluid_button_volume\b/g, 'fluid_button_mute'); } if (menuOptionMute !== null) { menuOptionMute.innerHTML = self.displayOptions.captions.unmute; } } currentVolumeTag.style.width = (self.domRef.player.volume * volumebarTotalWidth) + 'px'; volumeposTag.style.left = (self.domRef.player.volume * volumebarTotalWidth - (volumeposTagWidth / 2)) + 'px'; }; self.muteToggle = () => { if (0 !== self.domRef.player.volume && !self.domRef.player.muted) { self.domRef.player.volume = 0; self.domRef.player.muted = true; } else { self.domRef.player.volume = self.latestVolume; self.domRef.player.muted = false; } // Persistent settings self.fluidStorage.fluidVolume = self.latestVolume; self.fluidStorage.fluidMute = self.domRef.player.muted; self.trackMuteChange(); }; self.checkFullscreenSupport = () => { const videoPlayerWrapper = self.domRef.wrapper; if (videoPlayerWrapper.mozRequestFullScreen) { return { goFullscreen: 'mozRequestFullScreen', exitFullscreen: 'mozCancelFullScreen', isFullscreen: 'mozFullScreenElement' }; } else if (videoPlayerWrapper.webkitRequestFullscreen) { return { goFullscreen: 'webkitRequestFullscreen', exitFullscreen: 'webkitExitFullscreen', isFullscreen: 'webkitFullscreenElement' }; } else if (videoPlayerWrapper.msRequestFullscreen) { return { goFullscreen: 'msRequestFullscreen', exitFullscreen: 'msExitFullscreen', isFullscreen: 'msFullscreenElement' }; } else if (videoPlayerWrapper.requestFullscreen) { return { goFullscreen: 'requestFullscreen', exitFullscreen: 'exitFullscreen', isFullscreen: 'fullscreenElement' }; } else if (self.domRef.player.webkitSupportsFullscreen) { return { goFullscreen: 'webkitEnterFullscreen', exitFullscreen: 'webkitExitFullscreen', isFullscreen: 'webkitDisplayingFullscreen' }; } return false; }; self.fullscreenOff = (fullscreenButton, menuOptionFullscreen) => { for (let i = 0; i < fullscreenButton.length; i++) { fullscreenButton[i].className = fullscreenButton[i].className.replace(/\bfluid_button_fullscreen_exit\b/g, 'fluid_button_fullscreen'); } if (menuOptionFullscreen !== null) { menuOptionFullscreen.innerHTML = 'Fullscreen'; } self.fullscreenMode = false; }; self.fullscreenOn = (fullscreenButton, menuOptionFullscreen) => { for (let i = 0; i < fullscreenButton.length; i++) { fullscreenButton[i].className = fullscreenButton[i].className.replace(/\bfluid_button_fullscreen\b/g, 'fluid_button_fullscreen_exit'); } if (menuOptionFullscreen !== null) { menuOptionFullscreen.innerHTML = self.displayOptions.captions.exitFullscreen; } self.fullscreenMode = true; }; self.fullscreenToggle = (toAnotherDisplayTarget = false) => { self.debugMessage(`Toggling Full Screen`); const videoPlayerTag = self.domRef.player; const fullscreenTag = self.domRef.wrapper; const requestFullscreenFunctionNames = self.checkFullscreenSupport(); const fullscreenButton = videoPlayerTag.parentNode.getElementsByClassName('fluid_control_fullscreen'); const menuOptionFullscreen = fullscreenTag.querySelector('.context_option_fullscreen'); const previousDisplayMode = self.getPreviousDisplayMode(); self.resetDisplayMode(displayModes.FULLSCREEN); let functionNameToExecute; if (requestFullscreenFunctionNames) { // iOS fullscreen elements are different and so need to be treated separately if (requestFullscreenFunctionNames.goFullscreen === 'webkitEnterFullscreen') { if (!videoPlayerTag[requestFullscreenFunctionNames.isFullscreen]) { functionNameToExecute = 'videoPlayerTag.' + requestFullscreenFunctionNames.goFullscreen + '();'; self.fullscreenOn(fullscreenButton, menuOptionFullscreen); new Function('videoPlayerTag', functionNameToExecute)(videoPlayerTag); } } else { if (document[requestFullscreenFunctionNames.isFullscreen] === null) { //Go fullscreen functionNameToExecute = 'videoPlayerTag.' + requestFullscreenFunctionNames.goFullscreen + '();'; self.fullscreenOn(fullscreenButton, menuOptionFullscreen); } else { //Exit fullscreen functionNameToExecute = 'document.' + requestFullscreenFunctionNames.exitFullscreen + '();'; self.fullscreenOff(fullscreenButton, menuOptionFullscreen); } new Function('videoPlayerTag', functionNameToExecute)(fullscreenTag); } } else { //The browser does not support the Fullscreen API, so a pseudo-fullscreen implementation is used if (fullscreenTag.className.search(/\bpseudo_fullscreen\b/g) !== -1) { fullscreenTag.className = fullscreenTag.className.replace(/\bpseudo_fullscreen\b/g, ''); self.fullscreenOff(fullscreenButton, menuOptionFullscreen); } else { fullscreenTag.className += ' pseudo_fullscreen'; self.fullscreenOn(fullscreenButton, menuOptionFullscreen); } } self.resizeVpaidAuto(); // Listen for fullscreen exit event on safari, as the fullscreen mode uses the native UI in iOS self.domRef.player.addEventListener('webkitendfullscreen', () => { self.fullscreenOff(fullscreenButton, menuOptionFullscreen); }); if (!toAnotherDisplayTarget) { self.trackPlayerSizeChanged(previousDisplayMode); } }; self.findClosestParent = (el, selector) => { let matchesFn = null; // find vendor prefix ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some(function (fn) { if (typeof document.body[fn] == 'function') { matchesFn = fn; return true; } return false; }); let parent; // Check if the current element matches the selector if (el[matchesFn](selector)) { return el; } // traverse parents while (el) { parent = el.parentElement; if (parent && parent[matchesFn](selector)) { return parent; } el = parent; } return null; }; self.getTranslateX = (el) => { let coordinates = null; try { const results = el.style.transform.match(/translate3d\((-?\d+px,\s?){2}-?\d+px\)/); if (results && results.length) { coordinates = results[0] .replace('translate3d(', '') .replace(')', '') .replace(/\s/g, '') .replace(/px/g, '') .split(',') ; } } catch (e) { coordinates = null; } return (coordinates && (coordinates.length === 3)) ? parseInt(coordinates[0]) : 0; }; self.getEventOffsetX = (evt, el) => { let x = 0; let translateX = 0; while (el &&