From 716e8c00e4a56be73e448c95b93c429ded120adf Mon Sep 17 00:00:00 2001 From: Roy Bregman <48884909+RoyBregman@users.noreply.github.com> Date: Thu, 26 Nov 2020 09:05:46 +0200 Subject: [PATCH] feat(FEC-10233): plugin async loading support (#372) --- docs/how-to-build-a-plugin.md | 9 ++ docs/playing-your-video.md | 1 - flow-typed/interfaces/middleware-provider.js | 2 +- src/common/plugins/base-plugin.js | 9 ++ .../plugins/plugin-readiness-middleware.js | 55 +++++++++ src/kaltura-player.js | 29 +++-- .../test-plugins/async-reject-plugin.js | 16 +++ .../test-plugins/async-resolve-plugin.js | 16 +++ test/src/kaltura-player.spec.js | 111 ++++++++++++++++++ 9 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 src/common/plugins/plugin-readiness-middleware.js create mode 100644 test/src/common/plugin/test-plugins/async-reject-plugin.js create mode 100644 test/src/common/plugin/test-plugins/async-resolve-plugin.js diff --git a/docs/how-to-build-a-plugin.md b/docs/how-to-build-a-plugin.md index 174b46bf2..42aa5a38a 100644 --- a/docs/how-to-build-a-plugin.md +++ b/docs/how-to-build-a-plugin.md @@ -175,6 +175,15 @@ The player will call this method before destroying itself. The player will call this method before changing media. +> #### `get ready()` +> +> Returns: `Promise<*>` - a Promise which is resolved when plugin is ready for the player load + +Signal player that plugin has finished loading its dependencies and player can continue to loading and playing states. +Use this when your plugin requires to load 3rd party dependencies which are required for the plugin operation. +By default the base plugin returns a resolved plugin. +If you wish the player load and play (and middlewares interception) to wait for some async action (i.e loading a 3rd party library) you can override and return a Promise which is resolved when the plugin completes all async requirements. + # Now, lets take a look at the `registerPlugin` API. diff --git a/docs/playing-your-video.md b/docs/playing-your-video.md index e8ab94c80..94d9e5dfc 100644 --- a/docs/playing-your-video.md +++ b/docs/playing-your-video.md @@ -109,7 +109,6 @@ var mediaInfo = { > Note: \*\*\* Either entryId or referenceId must be supplied (if both will be supplied, the media will be loaded by mediaId) - ## Examples Let's look at some examples. diff --git a/flow-typed/interfaces/middleware-provider.js b/flow-typed/interfaces/middleware-provider.js index 5f151255f..612b7bf31 100644 --- a/flow-typed/interfaces/middleware-provider.js +++ b/flow-typed/interfaces/middleware-provider.js @@ -1,5 +1,5 @@ // @flow -import BaseMiddleware from '@playkit-js/playkit-js'; +import {BaseMiddleware} from '@playkit-js/playkit-js'; declare interface IMiddlewareProvider { getMiddlewareImpl(): BaseMiddleware; diff --git a/src/common/plugins/base-plugin.js b/src/common/plugins/base-plugin.js index d39826cd4..7179dfbf6 100644 --- a/src/common/plugins/base-plugin.js +++ b/src/common/plugins/base-plugin.js @@ -96,6 +96,15 @@ export class BasePlugin implements IPlugin { return Utils.Object.copyDeep(this.config); } + /** + * Getter for the ready promise of the plugin. + * @returns {Promise<*>} - returns a resolved promise unless the plugin overrides this ready getter. + * @public + */ + get ready(): Promise<*> { + return Promise.resolve(); + } + /** * Updates the config of the plugin. * @param {Object} update - The updated configuration. diff --git a/src/common/plugins/plugin-readiness-middleware.js b/src/common/plugins/plugin-readiness-middleware.js new file mode 100644 index 000000000..e5733e674 --- /dev/null +++ b/src/common/plugins/plugin-readiness-middleware.js @@ -0,0 +1,55 @@ +//@flow + +import {BaseMiddleware, getLogger} from '@playkit-js/playkit-js'; +import {BasePlugin} from './base-plugin'; +class PluginReadinessMiddleware extends BaseMiddleware { + _plugins: Array; + id: string = 'PluginReadinessMiddleware'; + static _logger = getLogger('PluginReadinessMiddleware'); + + constructor(plugins: Array) { + super(); + this._plugins = plugins; + PluginReadinessMiddleware._logger.debug('plugins readiness', this._plugins); + } + + /** + * Load middleware handler. + * @param {Function} next - The load handler in the middleware chain. + * @returns {void} + * @memberof PluginReadinessMiddleware + */ + load(next: Function): void { + this._checkNextSettle(0, next); + } + _checkNextSettle(index: number, next: Function) { + if (index < this._plugins.length) { + this._checkSettle(index, next); + } else { + this.callNext(next); + } + } + + _checkSettle(index: number, next: Function) { + const readyPromise = this._plugins[index].ready ? this._plugins[index].ready : Promise.resolve(); + readyPromise + .then(() => { + PluginReadinessMiddleware._logger.debug(`plugin ${this._plugins[index].name} ready promise resolved`); + this._checkNextSettle(index + 1, next); + }) + .catch(() => { + PluginReadinessMiddleware._logger.debug(`plugin ${this._plugins[index].name} ready promise rejected`); + this._checkNextSettle(index + 1, next); + }); + } + /** + * Play middleware handler. + * @param {Function} next - The play handler in the middleware chain. + * @returns {void} + * @memberof PluginReadinessMiddleware + */ + play(next: Function): void { + this._checkNextSettle(0, next); + } +} +export {PluginReadinessMiddleware}; diff --git a/src/kaltura-player.js b/src/kaltura-player.js index 6a7abff1c..3b27926bc 100644 --- a/src/kaltura-player.js +++ b/src/kaltura-player.js @@ -32,6 +32,7 @@ import { getLogger, LogLevel } from '@playkit-js/playkit-js'; +import {PluginReadinessMiddleware} from './common/plugins/plugin-readiness-middleware'; class KalturaPlayer extends FakeEventTarget { static _logger: any = getLogger('KalturaPlayer' + Utils.Generator.uniqueId(5)); @@ -50,6 +51,7 @@ class KalturaPlayer extends FakeEventTarget { _reset: boolean = true; _firstPlay: boolean = true; _sourceSelected: boolean = false; + _pluginReadinessMiddleware: PluginReadinessMiddleware; constructor(options: KPOptionsObject) { super(); @@ -175,6 +177,7 @@ class KalturaPlayer extends FakeEventTarget { KalturaPlayer._logger.debug('loadPlaylistByEntryList', entryList); this._uiWrapper.setLoadingSpinnerState(true); const providerResult = this._provider.getEntryListConfig(entryList); + providerResult.then( playlistData => this.setPlaylist(playlistData, playlistConfig, entryList), e => @@ -687,27 +690,29 @@ class KalturaPlayer extends FakeEventTarget { } } - _configureOrLoadPlugins(plugins: Object = {}): void { + _configureOrLoadPlugins(pluginsConfig: Object = {}): void { const middlewares = []; const uiComponents = []; - Object.keys(plugins).forEach(name => { + const plugins = []; + Object.keys(pluginsConfig).forEach(name => { // If the plugin is already exists in the registry we are updating his config const plugin = this._pluginManager.get(name); if (plugin) { - plugin.updateConfig(plugins[name]); - plugins[name] = plugin.getConfig(); + plugin.updateConfig(pluginsConfig[name]); + pluginsConfig[name] = plugin.getConfig(); } else { - // We allow to load plugins as long as the player has no engine + // We allow to load pluginsConfig as long as the player has no engine if (!this._sourceSelected) { try { - this._pluginManager.load(name, this, plugins[name]); + this._pluginManager.load(name, this, pluginsConfig[name]); } catch (error) { //bounce the plugin load error up this.dispatchEvent(new FakeEvent(Error.Code.ERROR, error)); } let plugin = this._pluginManager.get(name); if (plugin) { - plugins[name] = plugin.getConfig(); + plugins.push(plugin); + pluginsConfig[name] = plugin.getConfig(); if (typeof plugin.getMiddlewareImpl === 'function') { // push the bumper middleware to the end, to play the bumper right before the content plugin.name === 'bumper' ? middlewares.push(plugin.getMiddlewareImpl()) : middlewares.unshift(plugin.getMiddlewareImpl()); @@ -722,13 +727,19 @@ class KalturaPlayer extends FakeEventTarget { } } } else { - delete plugins[name]; + delete pluginsConfig[name]; } } }); this._pluginsUiComponents = uiComponents; + + // First in the middleware chain is the plugin readiness to insure plugins are ready before load / play + if (!this._pluginReadinessMiddleware) { + this._pluginReadinessMiddleware = new PluginReadinessMiddleware(plugins); + this._localPlayer.playbackMiddleware.use(this._pluginReadinessMiddleware); + } middlewares.forEach(middleware => this._localPlayer.playbackMiddleware.use(middleware)); - Utils.Object.mergeDeep(this._pluginsConfig, plugins); + Utils.Object.mergeDeep(this._pluginsConfig, pluginsConfig); } _maybeCreateAdsController(): void { diff --git a/test/src/common/plugin/test-plugins/async-reject-plugin.js b/test/src/common/plugin/test-plugins/async-reject-plugin.js new file mode 100644 index 000000000..78e22887c --- /dev/null +++ b/test/src/common/plugin/test-plugins/async-reject-plugin.js @@ -0,0 +1,16 @@ +import {BasePlugin} from '../../../../../src/common/plugins'; + +export default class AsyncRejectPlugin extends BasePlugin { + static DELAY_ASYNC = 300; + static isValid(): boolean { + return true; + } + + get ready(): Promise<*> { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(); + }, AsyncRejectPlugin.DELAY_ASYNC); + }); + } +} diff --git a/test/src/common/plugin/test-plugins/async-resolve-plugin.js b/test/src/common/plugin/test-plugins/async-resolve-plugin.js new file mode 100644 index 000000000..da379e212 --- /dev/null +++ b/test/src/common/plugin/test-plugins/async-resolve-plugin.js @@ -0,0 +1,16 @@ +import {BasePlugin} from '../../../../../src/common/plugins'; + +export default class AsyncResolvePlugin extends BasePlugin { + static DELAY_ASYNC = 500; + static isValid(): boolean { + return true; + } + + get ready(): Promise<*> { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, AsyncResolvePlugin.DELAY_ASYNC); + }); + } +} diff --git a/test/src/kaltura-player.spec.js b/test/src/kaltura-player.spec.js index 1955c762a..a8e5ca584 100644 --- a/test/src/kaltura-player.spec.js +++ b/test/src/kaltura-player.spec.js @@ -8,6 +8,8 @@ import NumbersPlugin from './common/plugin/test-plugins/numbers-plugin'; import {KalturaPlayer as Player} from '../../src/kaltura-player'; import SourcesConfig from './configs/sources'; import {FakeEvent} from '@playkit-js/playkit-js'; +import AsyncResolvePlugin from './common/plugin/test-plugins/async-resolve-plugin'; +import AsyncRejectPlugin from './common/plugin/test-plugins/async-reject-plugin'; const targetId = 'player-placeholder_kaltura-player.spec'; @@ -612,6 +614,115 @@ describe('kaltura player api', function () { }); }); }); + describe('async plugins loading', () => { + let player; + let timeStart; + beforeEach(() => { + PluginManager.register('asyncResolve', AsyncResolvePlugin); + PluginManager.register('asyncReject', AsyncRejectPlugin); + }); + + afterEach(() => { + PluginManager.unRegister('asyncResolve'); + PluginManager.unRegister('asyncReject'); + }); + + it('should create player with async resolve plugin - check async load', done => { + player = new Player({ + ui: {}, + provider: {}, + sources: SourcesConfig.Mp4, + plugins: { + asyncResolve: {} + } + }); + player._pluginManager.get('asyncResolve').should.exist; + sinon.stub(player._localPlayer, '_load').callsFake(function () { + const timeDiff = Date.now() - timeStart; + timeDiff.should.gt(AsyncResolvePlugin.DELAY_ASYNC); + done(); + }); + timeStart = Date.now(); + player.load(); + }); + + it('should create player with async resolve plugin - check async play', done => { + player = new Player({ + ui: {}, + provider: {}, + sources: SourcesConfig.Mp4, + plugins: { + asyncResolve: {} + } + }); + player._pluginManager.get('asyncResolve').should.exist; + sinon.stub(player._localPlayer, '_play').callsFake(function () { + const timeDiff = Date.now() - timeStart; + timeDiff.should.gt(AsyncResolvePlugin.DELAY_ASYNC); + done(); + }); + timeStart = Date.now(); + player.play(); + }); + it('should create player with async reject plugin - check async load', done => { + player = new Player({ + ui: {}, + provider: {}, + sources: SourcesConfig.Mp4, + plugins: { + asyncReject: {} + } + }); + player._pluginManager.get('asyncReject').should.exist; + sinon.stub(player._localPlayer, '_load').callsFake(function () { + const timeDiff = Date.now() - timeStart; + timeDiff.should.gt(AsyncRejectPlugin.DELAY_ASYNC); + done(); + }); + timeStart = Date.now(); + player.load(); + }); + + it('should create player with async reject plugin - check async play', done => { + player = new Player({ + ui: {}, + provider: {}, + sources: SourcesConfig.Mp4, + plugins: { + asyncReject: {} + } + }); + player._pluginManager.get('asyncReject').should.exist; + sinon.stub(player._localPlayer, '_play').callsFake(function () { + const timeDiff = Date.now() - timeStart; + timeDiff.should.gt(AsyncRejectPlugin.DELAY_ASYNC); + done(); + }); + timeStart = Date.now(); + player.play(); + }); + + it('should create player with async resolve plugin and reject plugin - check async play', done => { + player = new Player({ + ui: {}, + provider: {}, + sources: SourcesConfig.Mp4, + plugins: { + asyncReject: {}, + asyncResolve: {} + } + }); + player._pluginManager.get('asyncReject').should.exist; + player._pluginManager.get('asyncResolve').should.exist; + sinon.stub(player._localPlayer, '_load').callsFake(function () { + const timeDiff = Date.now() - timeStart; + timeDiff.should.gt(Math.max(AsyncRejectPlugin.DELAY_ASYNC, AsyncResolvePlugin.DELAY_ASYNC)); + done(); + }); + timeStart = Date.now(); + player.load(); + }); + }); describe('events', function () { let player;