cloudinary-video-player
Version:
Cloudinary Video Player
748 lines (608 loc) • 18.4 kB
JavaScript
import videojs from 'video.js';
import isObj from 'is-obj';
import './components';
import * as plugins from 'plugins';
import * as Utils from 'utils';
import assign from 'utils/assign';
import defaults from 'config/defaults';
import Eventable from 'mixins/eventable';
import ExtendedEvents from 'extended-events';
import normalizeAttributes from './attributes-normalizer';
import PlaylistWidget from './components/playlist/playlist-widget';
import { cloudinaryErrorsConverter } from './plugins/cloudinary/common';
import {
CLASS_PREFIX,
skinClassPrefix,
setSkinClassPrefix,
playerClassPrefix
} from './utils/css-prefix';
const CLOUDINARY_PARAMS = [
'cloudinaryConfig',
'transformation',
'sourceTypes',
'sourceTransformation',
'posterOptions',
'autoShowRecommendations',
'fontFace'];
const PLAYER_PARAMS = CLOUDINARY_PARAMS.concat([
'publicId',
'source',
'autoplayMode',
'playedEventPercents',
'playedEventTimes',
'analytics',
'fluid',
'ima',
'playlistWidget',
'hideContextMenu',
'colors',
'floatingWhenNotVisible',
'ads',
'showJumpControls',
'textTracks'
]);
// Register all plugins
Object.keys(plugins).forEach((key) => {
videojs.registerPlugin(key, plugins[key]);
});
const normalizeAutoplay = (options) => {
const autoplayMode = options.autoplayMode;
if (autoplayMode) {
switch (autoplayMode) {
case 'always':
options.autoplay = true;
break;
case 'on-scroll':
case 'never':
default:
options.autoplay = false;
}
}
};
const resolveVideoElement = (elem) => {
if (typeof elem === 'string') {
let id = elem;
// Adjust for jQuery ID syntax
if (id.indexOf('#') === 0) {
id = id.slice(1);
}
elem = document.querySelector(`#${id}`);
if (!elem) {
throw new Error(`Could not find element with id ${id}`);
}
}
if (!elem.tagName) {
throw new Error('Must specify either an element or an element id.');
} else if (elem.tagName !== 'VIDEO') {
throw new Error('Element is not a video tag.');
}
return elem;
};
const extractOptions = (elem, options) => {
const elemOptions = normalizeAttributes(elem);
if (videojs.dom.hasClass(elem, 'cld-fluid') || videojs.dom.hasClass(elem, 'vjs-fluid')) {
options.fluid = true;
}
// Default options < Markup options < Player options
options = assign({}, defaults, elemOptions, options);
// In case of 'autoplay on scroll', we need to make sure normal HTML5 autoplay is off
normalizeAutoplay(options);
// VideoPlayer specific options
const playerOptions = Utils.sliceAndUnsetProperties(
options,
...PLAYER_PARAMS);
// Cloudinary plugin specific options
playerOptions.cloudinary = Utils.sliceAndUnsetProperties(
playerOptions,
...CLOUDINARY_PARAMS);
// Allow explicitly passing options to videojs using the `videojs` namespace, in order
// to avoid param name conflicts:
// VideoPlayer.new({ controls: true, videojs: { controls: false })
if (options.videojs) {
assign(options, options.videojs);
delete options.videojs;
}
return { playerOptions, videojsOptions: options };
};
const overrideDefaultVideojsComponents = () => {
const Player = videojs.getComponent('Player');
let children = Player.prototype.options_.children;
// Add TitleBar as default
children.push('titleBar');
children.push('upcomingVideoOverlay');
children.push('recommendationsOverlay');
const ControlBar = videojs.getComponent('ControlBar');
if (ControlBar) {
children = ControlBar.prototype.options_.children;
// Add space instead of the progress control (which we deattached from the controlBar, and absolutely positioned it above it)
// Also add a blank div underneath the progress control to stop bubbling up pointer events.
children.splice(children.indexOf('progressControl'), 0, 'spacer',
'progressControlEventsBlocker');
// Add 'play-previous' and 'play-next' buttons around the 'play-toggle'
children.splice(children.indexOf('playToggle'), 1, 'playlistPreviousButton', 'JumpBackButton', 'playToggle', 'JumpForwardButton', 'playlistNextButton');
// Position the 'cloudinary-button' button right next to 'fullscreenToggle'
children.splice(children.indexOf('fullscreenToggle'), 1, 'cloudinaryButton',
'fullscreenToggle');
}
};
overrideDefaultVideojsComponents();
let _allowUsageReport = true;
class VideoPlayer extends Utils.mixin(Eventable) {
constructor(elem, options, ready) {
super();
elem = resolveVideoElement(elem);
options = extractOptions(elem, options);
const onReady = () => {
setExtendedEvents();
this.fluid(_options.fluid);
// Load first video (mainly to support video tag 'source' and 'public-id' attributes)
const source = _options.source || _options.publicId;
if (source) {
this.source(source);
}
};
const setExtendedEvents = () => {
const events = [];
if (_options.playedEventPercents) {
const percentsplayed = {
type: 'percentsplayed',
percents: _options.playedEventPercents
};
events.push(percentsplayed);
}
if (_options.playedEventTimes) {
const timeplayed = {
type: 'timeplayed',
times: _options.playedEventTimes
};
events.push(timeplayed);
}
events.push(...['seek', 'mute', 'unmute', 'qualitychanged']);
const extendedEvents = new ExtendedEvents(this.videojs, { events });
const normalizedEvents = extendedEvents.events;
Object.keys(normalizedEvents).forEach((_event) => {
const handler = (event, data) => {
this.videojs.trigger({ type: _event, eventData: data });
};
extendedEvents.on(_event, handler);
});
};
const setCssClasses = () => {
this.videojs.addClass(CLASS_PREFIX);
this.videojs.addClass(playerClassPrefix(this.videojs));
setSkinClassPrefix(this.videojs, skinClassPrefix(this.videojs));
if (videojs.browser.IE_VERSION === 11) {
this.videojs.addClass('cld-ie11');
}
};
const initPlugins = (loaded) => {
this.adsEnabled = initIma(loaded);
initAutoplay();
initContextMenu();
initPerSrcBehaviors();
initCloudinary();
initAnalytics();
initFloatingPlayer();
initColors();
this.initTextTracks(options.videojsOptions.textTracks);
};
const initIma = (loaded) => {
if (!loaded.contribAdsLoaded || !loaded.imaAdsLoaded) {
if (_options.ads) {
if (!loaded.contribAdsLoaded) {
console.log('contribAds is not loaded');
}
if (!loaded.imaAdsLoaded) {
console.log('imaSdk is not loaded');
}
}
return false;
}
if (!_options.ads) {
_options.ads = {};
}
const opts = _options.ads;
if (Object.keys(opts).length === 0) {
return false;
}
const {
adTagUrl, prerollTimeout, postrollTimeout, showCountdown, adLabel,
autoPlayAdBreaks, locale
} = opts;
this.videojs.ima({
id: this.el().id,
adTagUrl,
disableFlashAds: true,
prerollTimeout: prerollTimeout || 5000,
postrollTimeout: postrollTimeout || 5000,
showCountdown: (showCountdown !== false),
adLabel: adLabel || 'Advertisement',
locale: locale || 'en',
autoPlayAdBreaks: (autoPlayAdBreaks !== false),
debug: true
});
return true;
};
const initAutoplay = () => {
const autoplayMode = _options.autoplayMode;
if (autoplayMode === 'on-scroll') {
this.videojs.autoplayOnScroll();
}
};
const initContextMenu = () => {
if (!options.playerOptions.hideContextMenu) {
this.videojs.contextMenu(defaults.contextMenu);
}
};
const initFloatingPlayer = () => {
if (options.playerOptions.floatingWhenNotVisible) {
this.videojs.floatingPlayer({ 'floatTo': options.playerOptions.floatingWhenNotVisible });
}
};
const initColors = () => {
this.videojs.colors(options.playerOptions.colors ? { 'colors': options.playerOptions.colors } : {});
};
const initPerSrcBehaviors = () => {
this.videojs.perSourceBehaviors();
};
const initCloudinary = () => {
const opts = _options.cloudinary;
opts.chainTarget = this;
this.videojs.cloudinary(_options.cloudinary);
};
const initAnalytics = () => {
const analyticsOpts = _options.analytics;
if (analyticsOpts) {
const opts = typeof analyticsOpts === 'object' ? analyticsOpts : {};
this.videojs.analytics(opts);
}
};
let _playlistWidget = null;
const initPlaylistWidget = () => {
this.videojs.on('playlistcreated', () => {
if (_playlistWidget) {
_playlistWidget.dispose();
}
const plwOptions = _options.playlistWidget;
if (isObj(plwOptions)) {
if (_options.fluid) {
plwOptions.fluid = true;
}
if (_options.cloudinary.fontFace) {
plwOptions.fontFace = _options.cloudinary.fontFace;
}
_playlistWidget = new PlaylistWidget(this.videojs, plwOptions);
}
});
};
const initJumpButtons = () => {
if (!_options.showJumpControls && this.videojs.controlBar) {
this.videojs.controlBar.removeChild('JumpForwardButton');
this.videojs.controlBar.removeChild('JumpBackButton');
}
};
this.initTextTracks = (conf) => {
if (conf) {
const tracks = Object.keys(conf);
for (const track of tracks) {
if (Array.isArray(conf[track])) {
const trks = conf[track];
for (let i = 0; i < trks.length; i++) {
let cnf = trks[i];
this.videojs.addRemoteTextTrack(buildTextTrackObj(track, cnf), true);
}
} else {
this.videojs.addRemoteTextTrack(buildTextTrackObj(track, conf[track]), true);
}
}
}
};
const buildTextTrackObj = (type, conf) => ({
kind: type,
label: conf.label,
srclang: conf.language,
default: !!(conf.default),
src: conf.url
});
const _options = options.playerOptions;
const _vjs_options = options.videojsOptions;
// Make sure to add 'video-js' class before creating videojs instance
Utils.addClass(elem, 'video-js');
Utils.fontFace(elem, _options);
this.videojs = videojs(elem, _vjs_options);
if (_vjs_options.muted) {
this.videojs.volume(0.4);
}
/* global google */
let loaded = {
contribAdsLoaded: typeof this.videojs.ads === 'function',
imaAdsLoaded: (typeof google === 'object' && typeof google.ima === 'object')
};
setCssClasses();
initPlugins(loaded);
initPlaylistWidget();
initJumpButtons();
this.fallbackTrys = 0;
this.videojs.on('error', () => {
if (this.videojs.error().code === 4 && this.fallbackTrys === 0) {
let currSrc = this.videojs.currentSource();
this.videojs.src(
currSrc.cldSrc.cloudinaryConfig().url(currSrc.cldSrc.publicId(), { resource_type: 'video' }) + '.mp4');
this.fallbackTrys++;
}
});
this.videojs.ready(() => {
onReady();
if (ready) {
ready(this);
}
});
if (this.adsEnabled) {
if (Object.keys(options.playerOptions.ads).length > 0 &&
typeof this.videojs.ima === 'object') {
if (options.playerOptions.ads.adsInPlaylist === 'first-video') {
this.videojs.one('sourcechanged', () => {
this.videojs.ima.playAd();
});
} else {
this.videojs.on('sourcechanged', () => {
this.videojs.ima.playAd();
});
}
}
}
this.nbCalls = 0;
this.reTryVideo = (maxNumberOfCalls, timeout) => {
if (!this.isVideoReady()) {
if (this.nbCalls < maxNumberOfCalls) {
this.nbCalls++;
this.videojs.setTimeout(this.reTryVideo, timeout);
} else {
let e = new Error('Video is not ready please try later');
this.videojs.trigger('error', e);
}
}
};
this.isVideoReady = () => {
let s = this.videojs.readyState();
if (s >= (/iPad|iPhone|iPod/.test(navigator.userAgent) ? 1 : 4)) {
this.nbCalls = 0;
return true;
}
return false;
};
this.playlistWidget = (options) => {
if (!options && !_playlistWidget) {
return false;
}
if (!options && _playlistWidget) {
return _playlistWidget;
}
if (isObj(options)) {
_playlistWidget.options(options);
}
return _playlistWidget;
};
}
static all(selector, ...args) {
const nodeList = document.querySelectorAll(selector);
const players = [];
for (let i = 0; i < nodeList.length; i++) {
players.push(new VideoPlayer(nodeList[i], ...args));
}
return players;
}
static allowUsageReport(bool) {
if (bool === undefined) {
return _allowUsageReport;
}
_allowUsageReport = !!bool;
return _allowUsageReport;
}
cloudinaryConfig(config) {
return this.videojs.cloudinary.cloudinaryConfig(config);
}
currentPublicId() {
return this.videojs.cloudinary.currentPublicId();
}
currentSourceUrl() {
return this.videojs.currentSource().src;
}
currentPoster() {
return this.videojs.cloudinary.currentPoster();
}
source(publicId, options = {}) {
if (VideoPlayer.allowUsageReport()) {
options.usageReport = true;
}
this.initTextTracks(options.textTracks);
clearTimeout(this.reTryVideo);
this.nbCalls = 0;
let maxTries = this.videojs.options_.maxTries || 3;
let videoReadyTimeout = this.videojs.options_.videoTimeout || 55000;
this.reTryVideo(maxTries, videoReadyTimeout);
let src = this.videojs.cloudinary.source(publicId, options);
let type = this.videojs.cloudinary.currentSourceType();
if (type === 'VideoSource' || type === 'AudioSource') {
this.testUrl(src.videojs.currentSrc());
}
return src;
}
testUrl(url) {
try {
let params = {
method: 'head',
uri: url
};
videojs.xhr(params, (err, resp) => {
if (err) {
this.videojs.error({ code: 10, message: err.message });
}
if (resp.statusCode !== 200) {
let headers = resp.headers;
let cldError = headers['x-cld-error'];
let cldName = this.cloudinaryConfig().config().cloud_name;
this.videojs.error(cloudinaryErrorsConverter(cldError, this.currentPublicId(), cldName));
this.videojs.reset();
}
});
} catch (e) {
this.videojs.error({ code: 10, message: e.message });
this.videojs.reset();
}
}
posterOptions(options) {
return this.videojs.cloudinary.posterOptions(options);
}
skin(name) {
if (name !== undefined && typeof name === 'string') {
setSkinClassPrefix(this.videojs, name);
if (this.playlistWidget()) {
this.playlistWidget().setSkin();
}
}
return skinClassPrefix(this.videojs);
}
playlist(sources, options = {}) {
return this.videojs.cloudinary.playlist(sources, options);
}
playlistByTag(tag, options = {}) {
return this.videojs.cloudinary.playlistByTag(tag, options);
}
sourcesByTag(tag, options = {}) {
return this.videojs.cloudinary.sourcesByTag(tag, options);
}
fluid(bool) {
if (bool === undefined) {
return this.videojs.fluid();
}
if (bool) {
this.videojs.addClass('cld-fluid');
} else {
this.videojs.removeClass('cld-fluid');
}
this.videojs.fluid(bool);
this.videojs.trigger('fluid', bool);
return this;
}
play() {
this.videojs.play();
return this;
}
stop() {
this.pause();
this.currentTime(0);
return this;
}
playPrevious() {
this.playlist().playPrevious();
return this;
}
playNext() {
this.playlist().playNext();
return this;
}
transformation(trans) {
return this.videojs.cloudinary.transformation(trans);
}
sourceTypes(types) {
return this.videojs.cloudinary.sourceTypes(types);
}
sourceTransformation(trans) {
return this.videojs.cloudinary.sourceTransformation(trans);
}
autoShowRecommendations(autoShow) {
return this.videojs.cloudinary.autoShowRecommendations(autoShow);
}
duration() {
return this.videojs.duration();
}
height(dimension) {
if (!dimension) {
return this.videojs.height();
}
this.videojs.height(dimension);
return this;
}
width(dimension) {
if (!dimension) {
return this.videojs.width();
}
this.videojs.width(dimension);
return this;
}
volume(volume) {
if (!volume) {
return this.videojs.volume();
}
this.videojs.volume(volume);
return this;
}
mute() {
if (!this.isMuted()) {
this.videojs.muted(true);
}
return this;
}
unmute() {
if (this.isMuted()) {
this.videojs.muted(false);
}
return this;
}
isMuted() {
return this.videojs.muted();
}
pause() {
this.videojs.pause();
return this;
}
currentTime(offsetSeconds) {
if (!offsetSeconds && offsetSeconds !== 0) {
return this.videojs.currentTime();
}
this.videojs.currentTime(offsetSeconds);
return this;
}
maximize() {
if (!this.isMaximized()) {
this.videojs.requestFullscreen();
}
return this;
}
exitMaximize() {
if (this.isMaximized()) {
this.videojs.exitFullscreen();
}
return this;
}
isMaximized() {
return this.videojs.isFullscreen();
}
dispose() {
this.videojs.dispose();
}
controls(bool) {
if (bool === undefined) {
return this.videojs.controls();
}
this.videojs.controls(bool);
return this;
}
ima() {
return {
playAd: this.videojs.ima.playAd
};
}
loop(bool) {
if (bool === undefined) {
return this.videojs.loop();
}
this.videojs.loop(bool);
return this;
}
el() {
return this.videojs.el();
}
}
export default VideoPlayer;