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