/**
* @file plugin.js
*/
import evented from './mixins/evented';
import stateful from './mixins/stateful';
import * as Events from './utils/events';
import log from './utils/log';
import Player from './player';
/**
* The base plugin name.
*
* @private
* @constant
* @type {string}
*/
const BASE_PLUGIN_NAME = 'plugin';
/**
* The key on which a player's active plugins cache is stored.
*
* @private
* @constant
* @type {string}
*/
const PLUGIN_CACHE_KEY = 'activePlugins_';
/**
* Stores registered plugins in a private space.
*
* @private
* @type {Object}
*/
const pluginStorage = {};
/**
* Reports whether or not a plugin has been registered.
*
* @private
* @param {string} name
* The name of a plugin.
*
* @return {boolean}
* Whether or not the plugin has been registered.
*/
const pluginExists = (name) => pluginStorage.hasOwnProperty(name);
/**
* Get a single registered plugin by name.
*
* @private
* @param {string} name
* The name of a plugin.
*
* @return {typeof Plugin|Function|undefined}
* The plugin (or undefined).
*/
const getPlugin = (name) => pluginExists(name) ? pluginStorage[name] : undefined;
/**
* Marks a plugin as "active" on a player.
*
* Also, ensures that the player has an object for tracking active plugins.
*
* @private
* @param {Player} player
* A Video.js player instance.
*
* @param {string} name
* The name of a plugin.
*/
const markPluginAsActive = (player, name) => {
player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
player[PLUGIN_CACHE_KEY][name] = true;
};
/**
* Triggers a pair of plugin setup events.
*
* @private
* @param {Player} player
* A Video.js player instance.
*
* @param {PluginEventHash} hash
* A plugin event hash.
*
* @param {boolean} [before]
* If true, prefixes the event name with "before". In other words,
* use this to trigger "beforepluginsetup" instead of "pluginsetup".
*/
const triggerSetupEvent = (player, hash, before) => {
const eventName = (before ? 'before' : '') + 'pluginsetup';
player.trigger(eventName, hash);
player.trigger(eventName + ':' + hash.name, hash);
};
/**
* Takes a basic plugin function and returns a wrapper function which marks
* on the player that the plugin has been activated.
*
* @private
* @param {string} name
* The name of the plugin.
*
* @param {Function} plugin
* The basic plugin.
*
* @return {Function}
* A wrapper function for the given plugin.
*/
const createBasicPlugin = function(name, plugin) {
const basicPluginWrapper = function() {
// We trigger the "beforepluginsetup" and "pluginsetup" events on the player
// regardless, but we want the hash to be consistent with the hash provided
// for advanced plugins.
//
// The only potentially counter-intuitive thing here is the `instance` in
// the "pluginsetup" event is the value returned by the `plugin` function.
triggerSetupEvent(this, {name, plugin, instance: null}, true);
const instance = plugin.apply(this, arguments);
markPluginAsActive(this, name);
triggerSetupEvent(this, {name, plugin, instance});
return instance;
};
Object.keys(plugin).forEach(function(prop) {
basicPluginWrapper[prop] = plugin[prop];
});
return basicPluginWrapper;
};
/**
* Takes a plugin sub-class and returns a factory function for generating
* instances of it.
*
* This factory function will replace itself with an instance of the requested
* sub-class of Plugin.
*
* @private
* @param {string} name
* The name of the plugin.
*
* @param {Plugin} PluginSubClass
* The advanced plugin.
*
* @return {Function}
*/
const createPluginFactory = (name, PluginSubClass) => {
// Add a `name` property to the plugin prototype so that each plugin can
// refer to itself by name.
PluginSubClass.prototype.name = name;
return function(...args) {
triggerSetupEvent(this, {name, plugin: PluginSubClass, instance: null}, true);
const instance = new PluginSubClass(...[this, ...args]);
// The plugin is replaced by a function that returns the current instance.
this[name] = () => instance;
triggerSetupEvent(this, instance.getEventHash());
return instance;
};
};
/**
* Parent class for all advanced plugins.
*
* @mixes module:evented~EventedMixin
* @mixes module:stateful~StatefulMixin
* @fires Player#beforepluginsetup
* @fires Player#beforepluginsetup:$name
* @fires Player#pluginsetup
* @fires Player#pluginsetup:$name
* @listens Player#dispose
* @throws {Error}
* If attempting to instantiate the base {@link Plugin} class
* directly instead of via a sub-class.
*/
class Plugin {
/**
* Creates an instance of this class.
*
* Sub-classes should call `super` to ensure plugins are properly initialized.
*
* @param {Player} player
* A Video.js player instance.
*/
constructor(player) {
if (this.constructor === Plugin) {
throw new Error('Plugin must be sub-classed; not directly instantiated.');
}
this.player = player;
if (!this.log) {
this.log = this.player.log.createLogger(this.name);
}
// Make this object evented, but remove the added `trigger` method so we
// use the prototype version instead.
evented(this);
delete this.trigger;
stateful(this, this.constructor.defaultState);
markPluginAsActive(player, this.name);
// Auto-bind the dispose method so we can use it as a listener and unbind
// it later easily.
this.dispose = this.dispose.bind(this);
// If the player is disposed, dispose the plugin.
player.on('dispose', this.dispose);
}
/**
* Get the version of the plugin that was set on <pluginName>.VERSION
*/
version() {
return this.constructor.VERSION;
}
/**
* Each event triggered by plugins includes a hash of additional data with
* conventional properties.
*
* This returns that object or mutates an existing hash.
*
* @param {Object} [hash={}]
* An object to be used as event an event hash.
*
* @return {PluginEventHash}
* An event hash object with provided properties mixed-in.
*/
getEventHash(hash = {}) {
hash.name = this.name;
hash.plugin = this.constructor;
hash.instance = this;
return hash;
}
/**
* Triggers an event on the plugin object and overrides
* {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
*
* @param {string|Object} event
* An event type or an object with a type property.
*
* @param {Object} [hash={}]
* Additional data hash to merge with a
* {@link PluginEventHash|PluginEventHash}.
*
* @return {boolean}
* Whether or not default was prevented.
*/
trigger(event, hash = {}) {
return Events.trigger(this.eventBusEl_, event, this.getEventHash(hash));
}
/**
* Handles "statechanged" events on the plugin. No-op by default, override by
* subclassing.
*
* @abstract
* @param {Event} e
* An event object provided by a "statechanged" event.
*
* @param {Object} e.changes
* An object describing changes that occurred with the "statechanged"
* event.
*/
handleStateChanged(e) {}
/**
* Disposes a plugin.
*
* Subclasses can override this if they want, but for the sake of safety,
* it's probably best to subscribe the "dispose" event.
*
* @fires Plugin#dispose
*/
dispose() {
const {name, player} = this;
/**
* Signals that a advanced plugin is about to be disposed.
*
* @event Plugin#dispose
* @type {Event}
*/
this.trigger('dispose');
this.off();
player.off('dispose', this.dispose);
// Eliminate any possible sources of leaking memory by clearing up
// references between the player and the plugin instance and nulling out
// the plugin's state and replacing methods with a function that throws.
player[PLUGIN_CACHE_KEY][name] = false;
this.player = this.state = null;
// Finally, replace the plugin name on the player with a new factory
// function, so that the plugin is ready to be set up again.
player[name] = createPluginFactory(name, pluginStorage[name]);
}
/**
* Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
*
* @param {string|Function} plugin
* If a string, matches the name of a plugin. If a function, will be
* tested directly.
*
* @return {boolean}
* Whether or not a plugin is a basic plugin.
*/
static isBasic(plugin) {
const p = (typeof plugin === 'string') ? getPlugin(plugin) : plugin;
return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
}
/**
* Register a Video.js plugin.
*
* @param {string} name
* The name of the plugin to be registered. Must be a string and
* must not match an existing plugin or a method on the `Player`
* prototype.
*
* @param {typeof Plugin|Function} plugin
* A sub-class of `Plugin` or a function for basic plugins.
*
* @return {typeof Plugin|Function}
* For advanced plugins, a factory function for that plugin. For
* basic plugins, a wrapper function that initializes the plugin.
*/
static registerPlugin(name, plugin) {
if (typeof name !== 'string') {
throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
}
if (pluginExists(name)) {
log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
} else if (Player.prototype.hasOwnProperty(name)) {
throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
}
if (typeof plugin !== 'function') {
throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
}
pluginStorage[name] = plugin;
// Add a player prototype method for all sub-classed plugins (but not for
// the base Plugin class).
if (name !== BASE_PLUGIN_NAME) {
if (Plugin.isBasic(plugin)) {
Player.prototype[name] = createBasicPlugin(name, plugin);
} else {
Player.prototype[name] = createPluginFactory(name, plugin);
}
}
return plugin;
}
/**
* De-register a Video.js plugin.
*
* @param {string} name
* The name of the plugin to be de-registered. Must be a string that
* matches an existing plugin.
*
* @throws {Error}
* If an attempt is made to de-register the base plugin.
*/
static deregisterPlugin(name) {
if (name === BASE_PLUGIN_NAME) {
throw new Error('Cannot de-register base plugin.');
}
if (pluginExists(name)) {
delete pluginStorage[name];
delete Player.prototype[name];
}
}
/**
* Gets an object containing multiple Video.js plugins.
*
* @param {Array} [names]
* If provided, should be an array of plugin names. Defaults to _all_
* plugin names.
*
* @return {Object|undefined}
* An object containing plugin(s) associated with their name(s) or
* `undefined` if no matching plugins exist).
*/
static getPlugins(names = Object.keys(pluginStorage)) {
let result;
names.forEach(name => {
const plugin = getPlugin(name);
if (plugin) {
result = result || {};
result[name] = plugin;
}
});
return result;
}
/**
* Gets a plugin's version, if available
*
* @param {string} name
* The name of a plugin.
*
* @return {string}
* The plugin's version or an empty string.
*/
static getPluginVersion(name) {
const plugin = getPlugin(name);
return plugin && plugin.VERSION || '';
}
}
/**
* Gets a plugin by name if it exists.
*
* @static
* @method getPlugin
* @memberOf Plugin
* @param {string} name
* The name of a plugin.
*
* @returns {typeof Plugin|Function|undefined}
* The plugin (or `undefined`).
*/
Plugin.getPlugin = getPlugin;
/**
* The name of the base plugin class as it is registered.
*
* @type {string}
*/
Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
/**
* Documented in player.js
*
* @ignore
*/
Player.prototype.usingPlugin = function(name) {
return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
};
/**
* Documented in player.js
*
* @ignore
*/
Player.prototype.hasPlugin = function(name) {
return !!pluginExists(name);
};
export default Plugin;
/**
* Signals that a plugin is about to be set up on a player.
*
* @event Player#beforepluginsetup
* @type {PluginEventHash}
*/
/**
* Signals that a plugin is about to be set up on a player - by name. The name
* is the name of the plugin.
*
* @event Player#beforepluginsetup:$name
* @type {PluginEventHash}
*/
/**
* Signals that a plugin has just been set up on a player.
*
* @event Player#pluginsetup
* @type {PluginEventHash}
*/
/**
* Signals that a plugin has just been set up on a player - by name. The name
* is the name of the plugin.
*
* @event Player#pluginsetup:$name
* @type {PluginEventHash}
*/
/**
* @typedef {Object} PluginEventHash
*
* @property {string} instance
* For basic plugins, the return value of the plugin function. For
* advanced plugins, the plugin instance on which the event is fired.
*
* @property {string} name
* The name of the plugin.
*
* @property {string} plugin
* For basic plugins, the plugin function. For advanced plugins, the
* plugin class/constructor.
*/