1. 1 : /**
  2. 2 : * @file plugin.js
  3. 3 : */
  4. 4 : import evented from './mixins/evented';
  5. 5 : import stateful from './mixins/stateful';
  6. 6 : import * as Events from './utils/events';
  7. 7 : import log from './utils/log';
  8. 8 : import Player from './player';
  9. 9 :
  10. 10 : /**
  11. 11 : * The base plugin name.
  12. 12 : *
  13. 13 : * @private
  14. 14 : * @constant
  15. 15 : * @type {string}
  16. 16 : */
  17. 17 : const BASE_PLUGIN_NAME = 'plugin';
  18. 18 :
  19. 19 : /**
  20. 20 : * The key on which a player's active plugins cache is stored.
  21. 21 : *
  22. 22 : * @private
  23. 23 : * @constant
  24. 24 : * @type {string}
  25. 25 : */
  26. 26 : const PLUGIN_CACHE_KEY = 'activePlugins_';
  27. 27 :
  28. 28 : /**
  29. 29 : * Stores registered plugins in a private space.
  30. 30 : *
  31. 31 : * @private
  32. 32 : * @type {Object}
  33. 33 : */
  34. 34 : const pluginStorage = {};
  35. 35 :
  36. 36 : /**
  37. 37 : * Reports whether or not a plugin has been registered.
  38. 38 : *
  39. 39 : * @private
  40. 40 : * @param {string} name
  41. 41 : * The name of a plugin.
  42. 42 : *
  43. 43 : * @return {boolean}
  44. 44 : * Whether or not the plugin has been registered.
  45. 45 : */
  46. 46 : const pluginExists = (name) => pluginStorage.hasOwnProperty(name);
  47. 47 :
  48. 48 : /**
  49. 49 : * Get a single registered plugin by name.
  50. 50 : *
  51. 51 : * @private
  52. 52 : * @param {string} name
  53. 53 : * The name of a plugin.
  54. 54 : *
  55. 55 : * @return {Function|undefined}
  56. 56 : * The plugin (or undefined).
  57. 57 : */
  58. 58 : const getPlugin = (name) => pluginExists(name) ? pluginStorage[name] : undefined;
  59. 59 :
  60. 60 : /**
  61. 61 : * Marks a plugin as "active" on a player.
  62. 62 : *
  63. 63 : * Also, ensures that the player has an object for tracking active plugins.
  64. 64 : *
  65. 65 : * @private
  66. 66 : * @param {Player} player
  67. 67 : * A Video.js player instance.
  68. 68 : *
  69. 69 : * @param {string} name
  70. 70 : * The name of a plugin.
  71. 71 : */
  72. 72 : const markPluginAsActive = (player, name) => {
  73. 73 : player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
  74. 74 : player[PLUGIN_CACHE_KEY][name] = true;
  75. 75 : };
  76. 76 :
  77. 77 : /**
  78. 78 : * Triggers a pair of plugin setup events.
  79. 79 : *
  80. 80 : * @private
  81. 81 : * @param {Player} player
  82. 82 : * A Video.js player instance.
  83. 83 : *
  84. 84 : * @param {Plugin~PluginEventHash} hash
  85. 85 : * A plugin event hash.
  86. 86 : *
  87. 87 : * @param {boolean} [before]
  88. 88 : * If true, prefixes the event name with "before". In other words,
  89. 89 : * use this to trigger "beforepluginsetup" instead of "pluginsetup".
  90. 90 : */
  91. 91 : const triggerSetupEvent = (player, hash, before) => {
  92. 92 : const eventName = (before ? 'before' : '') + 'pluginsetup';
  93. 93 :
  94. 94 : player.trigger(eventName, hash);
  95. 95 : player.trigger(eventName + ':' + hash.name, hash);
  96. 96 : };
  97. 97 :
  98. 98 : /**
  99. 99 : * Takes a basic plugin function and returns a wrapper function which marks
  100. 100 : * on the player that the plugin has been activated.
  101. 101 : *
  102. 102 : * @private
  103. 103 : * @param {string} name
  104. 104 : * The name of the plugin.
  105. 105 : *
  106. 106 : * @param {Function} plugin
  107. 107 : * The basic plugin.
  108. 108 : *
  109. 109 : * @return {Function}
  110. 110 : * A wrapper function for the given plugin.
  111. 111 : */
  112. 112 : const createBasicPlugin = function(name, plugin) {
  113. 113 : const basicPluginWrapper = function() {
  114. 114 :
  115. 115 : // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
  116. 116 : // regardless, but we want the hash to be consistent with the hash provided
  117. 117 : // for advanced plugins.
  118. 118 : //
  119. 119 : // The only potentially counter-intuitive thing here is the `instance` in
  120. 120 : // the "pluginsetup" event is the value returned by the `plugin` function.
  121. 121 : triggerSetupEvent(this, {name, plugin, instance: null}, true);
  122. 122 :
  123. 123 : const instance = plugin.apply(this, arguments);
  124. 124 :
  125. 125 : markPluginAsActive(this, name);
  126. 126 : triggerSetupEvent(this, {name, plugin, instance});
  127. 127 :
  128. 128 : return instance;
  129. 129 : };
  130. 130 :
  131. 131 : Object.keys(plugin).forEach(function(prop) {
  132. 132 : basicPluginWrapper[prop] = plugin[prop];
  133. 133 : });
  134. 134 :
  135. 135 : return basicPluginWrapper;
  136. 136 : };
  137. 137 :
  138. 138 : /**
  139. 139 : * Takes a plugin sub-class and returns a factory function for generating
  140. 140 : * instances of it.
  141. 141 : *
  142. 142 : * This factory function will replace itself with an instance of the requested
  143. 143 : * sub-class of Plugin.
  144. 144 : *
  145. 145 : * @private
  146. 146 : * @param {string} name
  147. 147 : * The name of the plugin.
  148. 148 : *
  149. 149 : * @param {Plugin} PluginSubClass
  150. 150 : * The advanced plugin.
  151. 151 : *
  152. 152 : * @return {Function}
  153. 153 : */
  154. 154 : const createPluginFactory = (name, PluginSubClass) => {
  155. 155 :
  156. 156 : // Add a `name` property to the plugin prototype so that each plugin can
  157. 157 : // refer to itself by name.
  158. 158 : PluginSubClass.prototype.name = name;
  159. 159 :
  160. 160 : return function(...args) {
  161. 161 : triggerSetupEvent(this, {name, plugin: PluginSubClass, instance: null}, true);
  162. 162 :
  163. 163 : const instance = new PluginSubClass(...[this, ...args]);
  164. 164 :
  165. 165 : // The plugin is replaced by a function that returns the current instance.
  166. 166 : this[name] = () => instance;
  167. 167 :
  168. 168 : triggerSetupEvent(this, instance.getEventHash());
  169. 169 :
  170. 170 : return instance;
  171. 171 : };
  172. 172 : };
  173. 173 :
  174. 174 : /**
  175. 175 : * Parent class for all advanced plugins.
  176. 176 : *
  177. 177 : * @mixes module:evented~EventedMixin
  178. 178 : * @mixes module:stateful~StatefulMixin
  179. 179 : * @fires Player#beforepluginsetup
  180. 180 : * @fires Player#beforepluginsetup:$name
  181. 181 : * @fires Player#pluginsetup
  182. 182 : * @fires Player#pluginsetup:$name
  183. 183 : * @listens Player#dispose
  184. 184 : * @throws {Error}
  185. 185 : * If attempting to instantiate the base {@link Plugin} class
  186. 186 : * directly instead of via a sub-class.
  187. 187 : */
  188. 188 : class Plugin {
  189. 189 :
  190. 190 : /**
  191. 191 : * Creates an instance of this class.
  192. 192 : *
  193. 193 : * Sub-classes should call `super` to ensure plugins are properly initialized.
  194. 194 : *
  195. 195 : * @param {Player} player
  196. 196 : * A Video.js player instance.
  197. 197 : */
  198. 198 : constructor(player) {
  199. 199 : if (this.constructor === Plugin) {
  200. 200 : throw new Error('Plugin must be sub-classed; not directly instantiated.');
  201. 201 : }
  202. 202 :
  203. 203 : this.player = player;
  204. 204 :
  205. 205 : if (!this.log) {
  206. 206 : this.log = this.player.log.createLogger(this.name);
  207. 207 : }
  208. 208 :
  209. 209 : // Make this object evented, but remove the added `trigger` method so we
  210. 210 : // use the prototype version instead.
  211. 211 : evented(this);
  212. 212 : delete this.trigger;
  213. 213 :
  214. 214 : stateful(this, this.constructor.defaultState);
  215. 215 : markPluginAsActive(player, this.name);
  216. 216 :
  217. 217 : // Auto-bind the dispose method so we can use it as a listener and unbind
  218. 218 : // it later easily.
  219. 219 : this.dispose = this.dispose.bind(this);
  220. 220 :
  221. 221 : // If the player is disposed, dispose the plugin.
  222. 222 : player.on('dispose', this.dispose);
  223. 223 : }
  224. 224 :
  225. 225 : /**
  226. 226 : * Get the version of the plugin that was set on <pluginName>.VERSION
  227. 227 : */
  228. 228 : version() {
  229. 229 : return this.constructor.VERSION;
  230. 230 : }
  231. 231 :
  232. 232 : /**
  233. 233 : * Each event triggered by plugins includes a hash of additional data with
  234. 234 : * conventional properties.
  235. 235 : *
  236. 236 : * This returns that object or mutates an existing hash.
  237. 237 : *
  238. 238 : * @param {Object} [hash={}]
  239. 239 : * An object to be used as event an event hash.
  240. 240 : *
  241. 241 : * @return {Plugin~PluginEventHash}
  242. 242 : * An event hash object with provided properties mixed-in.
  243. 243 : */
  244. 244 : getEventHash(hash = {}) {
  245. 245 : hash.name = this.name;
  246. 246 : hash.plugin = this.constructor;
  247. 247 : hash.instance = this;
  248. 248 : return hash;
  249. 249 : }
  250. 250 :
  251. 251 : /**
  252. 252 : * Triggers an event on the plugin object and overrides
  253. 253 : * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
  254. 254 : *
  255. 255 : * @param {string|Object} event
  256. 256 : * An event type or an object with a type property.
  257. 257 : *
  258. 258 : * @param {Object} [hash={}]
  259. 259 : * Additional data hash to merge with a
  260. 260 : * {@link Plugin~PluginEventHash|PluginEventHash}.
  261. 261 : *
  262. 262 : * @return {boolean}
  263. 263 : * Whether or not default was prevented.
  264. 264 : */
  265. 265 : trigger(event, hash = {}) {
  266. 266 : return Events.trigger(this.eventBusEl_, event, this.getEventHash(hash));
  267. 267 : }
  268. 268 :
  269. 269 : /**
  270. 270 : * Handles "statechanged" events on the plugin. No-op by default, override by
  271. 271 : * subclassing.
  272. 272 : *
  273. 273 : * @abstract
  274. 274 : * @param {Event} e
  275. 275 : * An event object provided by a "statechanged" event.
  276. 276 : *
  277. 277 : * @param {Object} e.changes
  278. 278 : * An object describing changes that occurred with the "statechanged"
  279. 279 : * event.
  280. 280 : */
  281. 281 : handleStateChanged(e) {}
  282. 282 :
  283. 283 : /**
  284. 284 : * Disposes a plugin.
  285. 285 : *
  286. 286 : * Subclasses can override this if they want, but for the sake of safety,
  287. 287 : * it's probably best to subscribe the "dispose" event.
  288. 288 : *
  289. 289 : * @fires Plugin#dispose
  290. 290 : */
  291. 291 : dispose() {
  292. 292 : const {name, player} = this;
  293. 293 :
  294. 294 : /**
  295. 295 : * Signals that a advanced plugin is about to be disposed.
  296. 296 : *
  297. 297 : * @event Plugin#dispose
  298. 298 : * @type {EventTarget~Event}
  299. 299 : */
  300. 300 : this.trigger('dispose');
  301. 301 : this.off();
  302. 302 : player.off('dispose', this.dispose);
  303. 303 :
  304. 304 : // Eliminate any possible sources of leaking memory by clearing up
  305. 305 : // references between the player and the plugin instance and nulling out
  306. 306 : // the plugin's state and replacing methods with a function that throws.
  307. 307 : player[PLUGIN_CACHE_KEY][name] = false;
  308. 308 : this.player = this.state = null;
  309. 309 :
  310. 310 : // Finally, replace the plugin name on the player with a new factory
  311. 311 : // function, so that the plugin is ready to be set up again.
  312. 312 : player[name] = createPluginFactory(name, pluginStorage[name]);
  313. 313 : }
  314. 314 :
  315. 315 : /**
  316. 316 : * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
  317. 317 : *
  318. 318 : * @param {string|Function} plugin
  319. 319 : * If a string, matches the name of a plugin. If a function, will be
  320. 320 : * tested directly.
  321. 321 : *
  322. 322 : * @return {boolean}
  323. 323 : * Whether or not a plugin is a basic plugin.
  324. 324 : */
  325. 325 : static isBasic(plugin) {
  326. 326 : const p = (typeof plugin === 'string') ? getPlugin(plugin) : plugin;
  327. 327 :
  328. 328 : return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
  329. 329 : }
  330. 330 :
  331. 331 : /**
  332. 332 : * Register a Video.js plugin.
  333. 333 : *
  334. 334 : * @param {string} name
  335. 335 : * The name of the plugin to be registered. Must be a string and
  336. 336 : * must not match an existing plugin or a method on the `Player`
  337. 337 : * prototype.
  338. 338 : *
  339. 339 : * @param {Function} plugin
  340. 340 : * A sub-class of `Plugin` or a function for basic plugins.
  341. 341 : *
  342. 342 : * @return {Function}
  343. 343 : * For advanced plugins, a factory function for that plugin. For
  344. 344 : * basic plugins, a wrapper function that initializes the plugin.
  345. 345 : */
  346. 346 : static registerPlugin(name, plugin) {
  347. 347 : if (typeof name !== 'string') {
  348. 348 : throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
  349. 349 : }
  350. 350 :
  351. 351 : if (pluginExists(name)) {
  352. 352 : log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
  353. 353 : } else if (Player.prototype.hasOwnProperty(name)) {
  354. 354 : throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
  355. 355 : }
  356. 356 :
  357. 357 : if (typeof plugin !== 'function') {
  358. 358 : throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
  359. 359 : }
  360. 360 :
  361. 361 : pluginStorage[name] = plugin;
  362. 362 :
  363. 363 : // Add a player prototype method for all sub-classed plugins (but not for
  364. 364 : // the base Plugin class).
  365. 365 : if (name !== BASE_PLUGIN_NAME) {
  366. 366 : if (Plugin.isBasic(plugin)) {
  367. 367 : Player.prototype[name] = createBasicPlugin(name, plugin);
  368. 368 : } else {
  369. 369 : Player.prototype[name] = createPluginFactory(name, plugin);
  370. 370 : }
  371. 371 : }
  372. 372 :
  373. 373 : return plugin;
  374. 374 : }
  375. 375 :
  376. 376 : /**
  377. 377 : * De-register a Video.js plugin.
  378. 378 : *
  379. 379 : * @param {string} name
  380. 380 : * The name of the plugin to be de-registered. Must be a string that
  381. 381 : * matches an existing plugin.
  382. 382 : *
  383. 383 : * @throws {Error}
  384. 384 : * If an attempt is made to de-register the base plugin.
  385. 385 : */
  386. 386 : static deregisterPlugin(name) {
  387. 387 : if (name === BASE_PLUGIN_NAME) {
  388. 388 : throw new Error('Cannot de-register base plugin.');
  389. 389 : }
  390. 390 : if (pluginExists(name)) {
  391. 391 : delete pluginStorage[name];
  392. 392 : delete Player.prototype[name];
  393. 393 : }
  394. 394 : }
  395. 395 :
  396. 396 : /**
  397. 397 : * Gets an object containing multiple Video.js plugins.
  398. 398 : *
  399. 399 : * @param {Array} [names]
  400. 400 : * If provided, should be an array of plugin names. Defaults to _all_
  401. 401 : * plugin names.
  402. 402 : *
  403. 403 : * @return {Object|undefined}
  404. 404 : * An object containing plugin(s) associated with their name(s) or
  405. 405 : * `undefined` if no matching plugins exist).
  406. 406 : */
  407. 407 : static getPlugins(names = Object.keys(pluginStorage)) {
  408. 408 : let result;
  409. 409 :
  410. 410 : names.forEach(name => {
  411. 411 : const plugin = getPlugin(name);
  412. 412 :
  413. 413 : if (plugin) {
  414. 414 : result = result || {};
  415. 415 : result[name] = plugin;
  416. 416 : }
  417. 417 : });
  418. 418 :
  419. 419 : return result;
  420. 420 : }
  421. 421 :
  422. 422 : /**
  423. 423 : * Gets a plugin's version, if available
  424. 424 : *
  425. 425 : * @param {string} name
  426. 426 : * The name of a plugin.
  427. 427 : *
  428. 428 : * @return {string}
  429. 429 : * The plugin's version or an empty string.
  430. 430 : */
  431. 431 : static getPluginVersion(name) {
  432. 432 : const plugin = getPlugin(name);
  433. 433 :
  434. 434 : return plugin && plugin.VERSION || '';
  435. 435 : }
  436. 436 : }
  437. 437 :
  438. 438 : /**
  439. 439 : * Gets a plugin by name if it exists.
  440. 440 : *
  441. 441 : * @static
  442. 442 : * @method getPlugin
  443. 443 : * @memberOf Plugin
  444. 444 : * @param {string} name
  445. 445 : * The name of a plugin.
  446. 446 : *
  447. 447 : * @returns {Function|undefined}
  448. 448 : * The plugin (or `undefined`).
  449. 449 : */
  450. 450 : Plugin.getPlugin = getPlugin;
  451. 451 :
  452. 452 : /**
  453. 453 : * The name of the base plugin class as it is registered.
  454. 454 : *
  455. 455 : * @type {string}
  456. 456 : */
  457. 457 : Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
  458. 458 :
  459. 459 : Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
  460. 460 :
  461. 461 : /**
  462. 462 : * Documented in player.js
  463. 463 : *
  464. 464 : * @ignore
  465. 465 : */
  466. 466 : Player.prototype.usingPlugin = function(name) {
  467. 467 : return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
  468. 468 : };
  469. 469 :
  470. 470 : /**
  471. 471 : * Documented in player.js
  472. 472 : *
  473. 473 : * @ignore
  474. 474 : */
  475. 475 : Player.prototype.hasPlugin = function(name) {
  476. 476 : return !!pluginExists(name);
  477. 477 : };
  478. 478 :
  479. 479 : export default Plugin;
  480. 480 :
  481. 481 : /**
  482. 482 : * Signals that a plugin is about to be set up on a player.
  483. 483 : *
  484. 484 : * @event Player#beforepluginsetup
  485. 485 : * @type {Plugin~PluginEventHash}
  486. 486 : */
  487. 487 :
  488. 488 : /**
  489. 489 : * Signals that a plugin is about to be set up on a player - by name. The name
  490. 490 : * is the name of the plugin.
  491. 491 : *
  492. 492 : * @event Player#beforepluginsetup:$name
  493. 493 : * @type {Plugin~PluginEventHash}
  494. 494 : */
  495. 495 :
  496. 496 : /**
  497. 497 : * Signals that a plugin has just been set up on a player.
  498. 498 : *
  499. 499 : * @event Player#pluginsetup
  500. 500 : * @type {Plugin~PluginEventHash}
  501. 501 : */
  502. 502 :
  503. 503 : /**
  504. 504 : * Signals that a plugin has just been set up on a player - by name. The name
  505. 505 : * is the name of the plugin.
  506. 506 : *
  507. 507 : * @event Player#pluginsetup:$name
  508. 508 : * @type {Plugin~PluginEventHash}
  509. 509 : */
  510. 510 :
  511. 511 : /**
  512. 512 : * @typedef {Object} Plugin~PluginEventHash
  513. 513 : *
  514. 514 : * @property {string} instance
  515. 515 : * For basic plugins, the return value of the plugin function. For
  516. 516 : * advanced plugins, the plugin instance on which the event is fired.
  517. 517 : *
  518. 518 : * @property {string} name
  519. 519 : * The name of the plugin.
  520. 520 : *
  521. 521 : * @property {string} plugin
  522. 522 : * For basic plugins, the plugin function. For advanced plugins, the
  523. 523 : * plugin class/constructor.
  524. 524 : */