fluid-player
Version:
Fluid Player is a free HTML5 video player
1,280 lines (1,075 loc) • 137 kB
JavaScript
'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 &&