/** * @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 {Plugin~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 .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 {Plugin~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 Plugin~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 {Plugin~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 {Plugin~PluginEventHash} */ /** * Signals that a plugin has just been set up on a player. * * @event Player#pluginsetup * @type {Plugin~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 {Plugin~PluginEventHash} */ /** * @typedef {Object} Plugin~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. */