diff options
Diffstat (limited to 'javascript/videojs/src/js/plugin.js')
| -rw-r--r-- | javascript/videojs/src/js/plugin.js | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/plugin.js b/javascript/videojs/src/js/plugin.js new file mode 100644 index 0000000..52ab6e7 --- /dev/null +++ b/javascript/videojs/src/js/plugin.js @@ -0,0 +1,524 @@ +/** + * @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. + */ |
