UNPKG

fluid-player

Version:

Fluid Player is a free HTML5 video player

1,262 lines (1,055 loc) 119 kB
'use strict'; // 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' const FP_MODULES = [ VPAIDModule, VASTModule, CardboardModule, SubtitleModule, TimelineModule, AdSupportModule, StreamingModule, UtilsModule ]; // 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 + '"'; } playerNode.setAttribute('playsinline', ''); playerNode.setAttribute('webkit-playsinline', ''); 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; /** * @deprecated Nothing should RELY on this. An internal ID generator * should be used where absolutely necessary and DOM objects under FP control * MUST be referenced in domRef. */ self.videoPlayerId = playerNode.id; self.originalSrc = self.getCurrentSrc(); self.isCurrentlyPlayingAd = 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.adList = {}; 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 = {}; //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, showCardBoardView: false, showCardBoardJoystick: false, allowTheatre: true, doubleclickFullscreen: true, theatreSettings: { width: '100%', height: '60%', marginTop: 0, horizontalAlign: 'center', keepPosition: false }, theatreAdvanced: false, 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 }, 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 }, contextMenu: { controls: true, links: [] }, }, 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 () { }) } }, 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!') } // Overriding the default options for (let key in options) { if (!options.hasOwnProperty(key)) { continue; } if (typeof options[key] == "object") { for (let subKey in options[key]) { if (!options[key].hasOwnProperty(subKey)) { continue; } self.displayOptions[key][subKey] = options[key][subKey]; } } else { self.displayOptions[key] = options[key]; } } 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(); }); // '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(); // DO NOT initialize streamers if there are pre-rolls. It will break the streamers! // Streamers will re-initialize once ad has been shown. const preRolls = self.findRoll('preRoll'); if (!preRolls || 0 === preRolls.length) { 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(); } const videoWrapper = document.getElementById('fluid_video_wrapper_' + playerNode.id); if (!self.mobileInfo.userOs) { videoWrapper.addEventListener('mouseleave', self.handleMouseleave, false); videoWrapper.addEventListener('mouseenter', self.showControlBar, false); videoWrapper.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(); videoWrapper.addEventListener('touchstart', self.showControlBar, false); } //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 = document.getElementById('vast_video_loading_' + self.videoPlayerId); 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(); }; // 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) => { if (self.displayOptions.debug) { console.log(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(); } }; 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.id = 'vast_video_loading_' + self.videoPlayerId; controls.loader.style.display = 'none'; // Root element controls.root = document.createElement('div'); controls.root.className = 'fluid_controls_container'; controls.root.id = self.videoPlayerId + '_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.playPause.id = self.videoPlayerId + '_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.skipBack.id = self.videoPlayerId + '_fluid_control_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.skipForward.id = self.videoPlayerId + '_fluid_control_skip_forward'; controls.leftContainer.appendChild(controls.skipForward); } // Progress container controls.progressContainer = document.createElement('div'); controls.progressContainer.className = 'fluid_controls_progress_container fluid_slider'; controls.progressContainer.id = self.videoPlayerId + '_fluid_controls_progress_container'; 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.id = self.videoPlayerId + '_vast_control_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.progress_current_marker.id = self.videoPlayerId + '_vast_control_currentpos'; controls.progressCurrent.appendChild(controls.progress_current_marker); // Progress container -> Buffered indicator controls.bufferedIndicator = document.createElement('div'); controls.bufferedIndicator.className = 'fluid_controls_buffered'; controls.bufferedIndicator.id = self.videoPlayerId + '_buffered_amount'; controls.progressContainer.appendChild(controls.bufferedIndicator); // Progress container -> Ad markers controls.adMarkers = document.createElement('div'); controls.adMarkers.className = 'fluid_controls_ad_markers_holder'; controls.adMarkers.id = self.videoPlayerId + '_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.id = self.videoPlayerId + '_fluid_control_fullscreen'; controls.fullscreen.className = 'fluid_button fluid_control_fullscreen fluid_button_fullscreen'; controls.rightContainer.appendChild(controls.fullscreen); // Right container -> Theatre controls.theatre = document.createElement('div'); controls.theatre.id = self.videoPlayerId + '_fluid_control_theatre'; controls.theatre.className = 'fluid_button fluid_control_theatre fluid_button_theatre'; controls.rightContainer.appendChild(controls.theatre); // Right container -> Cardboard controls.cardboard = document.createElement('div'); controls.cardboard.id = self.videoPlayerId + '_fluid_control_cardboard'; controls.cardboard.className = 'fluid_button fluid_control_cardboard fluid_button_cardboard'; controls.rightContainer.appendChild(controls.cardboard); // Right container -> Subtitles controls.subtitles = document.createElement('div'); controls.subtitles.id = self.videoPlayerId + '_fluid_control_subtitles'; controls.subtitles.className = 'fluid_button fluid_button_subtitles'; controls.rightContainer.appendChild(controls.subtitles); // Right container -> Video source controls.videoSource = document.createElement('div'); controls.videoSource.id = self.videoPlayerId + '_fluid_control_video_source'; controls.videoSource.className = 'fluid_button fluid_button_video_source'; controls.rightContainer.appendChild(controls.videoSource); // Right container -> Playback rate controls.playbackRate = document.createElement('div'); controls.playbackRate.id = self.videoPlayerId + '_fluid_control_playback_rate'; controls.playbackRate.className = 'fluid_button fluid_button_playback_rate'; controls.rightContainer.appendChild(controls.playbackRate); // Right container -> Download controls.download = document.createElement('div'); controls.download.id = self.videoPlayerId + '_fluid_control_download'; controls.download.className = 'fluid_button fluid_button_download'; controls.rightContainer.appendChild(controls.download); // Right container -> Volume container controls.volumeContainer = document.createElement('div'); controls.volumeContainer.id = self.videoPlayerId + '_fluid_control_volume_container'; 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.id = self.videoPlayerId + '_fluid_control_volume'; controls.volume.className = 'fluid_control_volume'; controls.volumeContainer.appendChild(controls.volume); // Right container -> Volume container -> Volume -> Current controls.volumeCurrent = document.createElement('div'); controls.volumeCurrent.id = self.videoPlayerId + '_fluid_control_currentvolume'; 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.id = self.videoPlayerId + '_fluid_control_volume_currentpos'; controls.volumeCurrentPos.className = 'fluid_control_volume_currentpos'; controls.volumeCurrent.appendChild(controls.volumeCurrentPos); // Right container -> Volume container controls.mute = document.createElement('div'); controls.mute.id = self.videoPlayerId + '_fluid_control_mute'; controls.mute.className = 'fluid_button fluid_button_volume fluid_control_mute'; controls.rightContainer.appendChild(controls.mute); // Right container -> Volume container controls.duration = document.createElement('div'); controls.duration.id = self.videoPlayerId + '_fluid_control_duration'; controls.duration.className = 'fluid_control_duration fluid_fluid_control_duration'; controls.duration.innerText = '00:00 / 00:00'; controls.rightContainer.appendChild(controls.duration); return controls; }; self.controlPlayPauseToggle = () => { const playPauseButton = self.domRef.player.parentNode.getElementsByClassName('fluid_control_playpause'); const menuOptionPlay = document.getElementById(self.videoPlayerId + 'context_option_play'); const controlsDisplay = self.domRef.player.parentNode.getElementsByClassName('fluid_controls_container'); const fpLogo = document.getElementById(self.videoPlayerId + '_logo'); const initialPlay = document.getElementById(self.videoPlayerId + '_fluid_initial_play'); if (initialPlay) { document.getElementById(self.videoPlayerId + '_fluid_initial_play').style.display = "none"; document.getElementById(self.videoPlayerId + '_fluid_initial_play_button').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) { document.getElementById(self.videoPlayerId + '_fluid_initial_play').style.display = "block"; document.getElementById(self.videoPlayerId + '_fluid_initial_play_button').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; } if (play) { document.getElementById(self.videoPlayerId + '_fluid_state_button').classList.remove('fluid_initial_pause_button'); document.getElementById(self.videoPlayerId + '_fluid_state_button').classList.add('fluid_initial_play_button'); } else { document.getElementById(self.videoPlayerId + '_fluid_state_button').classList.remove('fluid_initial_play_button'); document.getElementById(self.videoPlayerId + '_fluid_state_button').classList.add('fluid_initial_pause_button'); } document.getElementById(self.videoPlayerId + '_fluid_initial_play').classList.add('transform-active'); setTimeout( function () { document.getElementById(self.videoPlayerId + '_fluid_initial_play').classList.remove('transform-active'); }, 800 ); }; self.contolProgressbarUpdate = () => { 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 = durationText; } }; self.contolVolumebarUpdate = () => { const currentVolumeTag = document.getElementById(self.videoPlayerId + '_fluid_control_currentvolume'); const volumeposTag = document.getElementById(self.videoPlayerId + '_fluid_control_volume_currentpos'); const volumebarTotalWidth = document.getElementById(self.videoPlayerId + '_fluid_control_volume').clientWidth; const volumeposTagWidth = volumeposTag.clientWidth; const muteButtonTag = self.domRef.player.parentNode.getElementsByClassName('fluid_control_mute'); const menuOptionMute = document.getElementById(self.videoPlayerId + '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.checkFullscreenSupport = (videoPlayerWrapperId) => { const videoPlayerWrapper = document.getElementById(videoPlayerWrapperId); 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 = () => { const videoPlayerTag = self.domRef.player; const fullscreenTag = document.getElementById('fluid_video_wrapper_' + self.videoPlayerId); const requestFullscreenFunctionNames = self.checkFullscreenSupport('fluid_video_wrapper_' + self.videoPlayerId); const fullscreenButton = videoPlayerTag.parentNode.getElementsByClassName('fluid_control_fullscreen'); const menuOptionFullscreen = document.getElementById(self.videoPlayerId + 'context_option_fullscreen'); // Disable Theatre mode if it's on while we toggle fullscreen if (self.theatreMode) { self.theatreToggle(); } 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(); }; 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 && !isNaN(el.offsetLeft)) { translateX = self.getTranslateX(el); if (el.tagName === 'BODY') { x += el.offsetLeft + el.clientLeft + translateX - (el.scrollLeft || document.documentElement.scrollLeft); } else { x += el.offsetLeft + el.clientLeft + translateX - el.scrollLeft; } el = el.offsetParent; } let eventX; if (typeof evt.touches !== 'undefined' && typeof evt.touches[0] !== 'undefined') { eventX = evt.touches[0].clientX; } else { eventX = evt.clientX } return eventX - x; }; self.getEventOffsetY = (evt, el) => { let fullscreenMultiplier = 1; const videoWrapper = self.findClosestParent(el, 'div[id^="fluid_video_wrapper_"]'); if (videoWrapper) { const videoPlayerId = videoWrapper.id.replace('fluid_video_wrapper_', ''); const requestFullscreenFunctionNames = self.checkFullscreenSupport('fluid_video_wrapper_' + videoPlayerId); if (requestFullscreenFunctionNames && document[requestFullscreenFunctionNames.isFullscreen]) { fullscreenMultiplier = 0; } } let y = 0; while (el && !isNaN(el.offsetTop)) { if (el.tagName === 'BODY') { y += el.offsetTop - ((el.scrollTop || document.documentElement.scrollTop) * fullscreenMultiplier); } else { y += el.offsetTop - (el.scrollTop * fullscreenMultiplier); } el = el.offsetParent; } return evt.clientY - y; }; self.onProgressbarMouseDown = (event) => { self.displayOptions.layoutControls.playPauseAnimation = false; // we need an initial position for touchstart events, as mouse up has no offset x for iOS let initialPosition; if (self.displayOptions.layoutControls.showCardBoardView) { initialPosition = self.getEventOffsetX(event, event.target.parentNode); } else { initialPosition = self.getEventOffsetX(event, document.getElementById(self.videoPlayerId + '_fluid_controls_progress_container')); } if (self.isCurrentlyPlayingAd) { return; } self.fluidPseudoPause = true; const initiallyPaused = self.domRef.player.paused; if (!initiallyPaused) { self.domRef.player.pause(); } const shiftTime = timeBarX => { const totalWidth = document.getElementById(self.videoPlayerId + '_fluid_controls_progress_container').clientWidth; if (totalWidth) { self.domRef.player.currentTime = self.currentVideoDuration * timeBarX / totalWidth; } }; const onProgressbarMouseMove = event => { const currentX = self.getEventOffsetX(event, event.target.parentNode); initialPosition = NaN; // mouse up will fire after the move, we don't want to trigger the initial position in the event of iOS shiftTime(currentX); self.contolProgressbarUpdate(self.videoPlayerId); self.controlDurationUpdate(self.videoPlayerId); }; const onProgressbarMouseUp = event => { document.removeEventListener('mousemove', onProgressbarMouseMove); document.removeEventListener('touchmove', onProgressbarMouseMove); document.removeEventListener('mouseup', onProgressbarMouseUp); document.removeEventListener('touchend', onProgressbarMouseUp); let clickedX = self.getEventOffsetX(event, event.target.parentNode); if (isNaN(clickedX) && !isNaN(initialPosition)) { clickedX = initialPosition; } if (!isNaN(clickedX)) { shiftTime(clickedX); } if (!initiallyPaused) { self.play(); } // Wait till video played then re-enable the animations if (self.initialAnimationSet) { setTimeout(() => { self.displayOptions.layoutControls.playPauseAnimation = self.initialAnimationSet; }, 200); } self.fluidPseudoPause = false; }; document.addEventListener('mouseup', onProgressbarMouseUp); document.addEventListener('touchend', onProgressbarMouseUp); document.addEventListener('mousemove', onProgressbarMouseMove); document.addEventListener('touchmove', onProgressbarMouseMove); }; self.onVolumeBarMouseDown = () => { const shiftVolume = volumeBarX => { const to