Skip to content

Commit

Permalink
feat(FEC-10233): plugin async loading support (#372)
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyBregman authored Nov 26, 2020
1 parent 0b40635 commit 716e8c0
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 11 deletions.
9 changes: 9 additions & 0 deletions docs/how-to-build-a-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion docs/playing-your-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion flow-typed/interfaces/middleware-provider.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import BaseMiddleware from '@playkit-js/playkit-js';
import {BaseMiddleware} from '@playkit-js/playkit-js';

declare interface IMiddlewareProvider {
getMiddlewareImpl(): BaseMiddleware;
Expand Down
9 changes: 9 additions & 0 deletions src/common/plugins/base-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 55 additions & 0 deletions src/common/plugins/plugin-readiness-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//@flow

import {BaseMiddleware, getLogger} from '@playkit-js/playkit-js';
import {BasePlugin} from './base-plugin';
class PluginReadinessMiddleware extends BaseMiddleware {
_plugins: Array<BasePlugin>;
id: string = 'PluginReadinessMiddleware';
static _logger = getLogger('PluginReadinessMiddleware');

constructor(plugins: Array<BasePlugin>) {
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};
29 changes: 20 additions & 9 deletions src/kaltura-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -50,6 +51,7 @@ class KalturaPlayer extends FakeEventTarget {
_reset: boolean = true;
_firstPlay: boolean = true;
_sourceSelected: boolean = false;
_pluginReadinessMiddleware: PluginReadinessMiddleware;

constructor(options: KPOptionsObject) {
super();
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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());
Expand All @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions test/src/common/plugin/test-plugins/async-reject-plugin.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
16 changes: 16 additions & 0 deletions test/src/common/plugin/test-plugins/async-resolve-plugin.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
111 changes: 111 additions & 0 deletions test/src/kaltura-player.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 716e8c0

Please sign in to comment.