Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FEC-10233): plugin async loading support #372

Merged
merged 17 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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