@skylineos/videojs-clsp
Version:
Video JS plugin for Skyline Technology Solutions' CLSP Player - https://github.com/skylineos/clsp-player
384 lines (295 loc) • 10.8 kB
JavaScript
// This is configured as an external library by webpack, so the caller must
// provide videojs on `window`
import videojs from 'video.js';
import ClspSourceHandler from './ClspSourceHandler';
import utils from './utils';
const Plugin = videojs.getPlugin('plugin');
// Note that the value can never be zero!
const VIDEOJS_ERRORS_PLAYER_CURRENT_TIME_MIN = 1;
const VIDEOJS_ERRORS_PLAYER_CURRENT_TIME_MAX = 20;
const logger = utils.Logger().factory('videojs-clsp');
let totalPluginCount = 0;
export default (defaultOptions = {}) => class ClspPlugin extends Plugin {
static VERSION = utils.version;
static utils = utils;
static METRIC_TYPES = [
'videojs.errorRetriesCount',
];
static register () {
if (videojs.getPlugin(utils.name)) {
throw new Error('You can only register the CLSP VideoJS Plugin once, and it has already been registered.');
}
const sourceHandler = ClspSourceHandler()('html5');
videojs.getTech('Html5').registerSourceHandler(sourceHandler, 0);
videojs.registerPlugin(utils.pluginName, ClspPlugin);
logger.debug('plugin registered');
return ClspPlugin;
}
static getDefaultOptions () {
return {
/**
* The number of times to retry playing the video when there is an error
* that we know we can recover from.
*
* If a negative number is passed, retry indefinitely
* If 0 is passed, never retry
* If a positive number is passed, retry that many times
*/
maxRetriesOnError: -1,
tourDuration: 10 * 1000,
enableMetrics: false,
videojsErrorsOptions: {},
};
}
constructor (player, options) {
super(player, options);
this.id = ++totalPluginCount;
this.logger = utils.Logger().factory(`CLSP VideoJS Plugin ${this.id}`);
this.logger.debug('creating plugin instance');
const playerOptions = player.options_;
this.options = videojs.mergeOptions({
...this.constructor.getDefaultOptions(),
...defaultOptions,
...(playerOptions.clsp || {}),
}, options);
this._playerOptions = playerOptions;
this.currentSourceIndex = 0;
player.addClass('vjs-clsp');
if (this.options.customClass) {
player.addClass(this.options.customClass);
}
this.resetErrors(player);
// @todo - this error doesn't work or display the way it's intended to
if (!utils.supported()) {
return player.error({
code: 'PLAYER_ERR_NOT_COMPAT',
type: 'PLAYER_ERR_NOT_COMPAT',
dismiss: false,
});
}
this.autoplayEnabled = playerOptions.autoplay || player.getAttribute('autoplay') === 'true';
// for debugging...
// const oldTrigger = player.trigger.bind(player);
// player.trigger = (eventName, ...args) => {
// console.log(eventName);
// console.log(...args);
// oldTrigger(eventName, ...args);
// };
// Track the number of times we've retried on error
player._errorRetriesCount = 0;
// Needed to make videojs-errors think that the video is progressing.
// If we do not do this, videojs-errors will give us a timeout error.
// The number just needs to change, it doesn't need to continually increment
player._currentTime = VIDEOJS_ERRORS_PLAYER_CURRENT_TIME_MIN;
player.currentTime = () => {
// Don't let this number get over 2 billion!
if (player._currentTime > VIDEOJS_ERRORS_PLAYER_CURRENT_TIME_MAX) {
player._currentTime = VIDEOJS_ERRORS_PLAYER_CURRENT_TIME_MIN;
}
else {
player._currentTime++;
}
return player._currentTime;
};
// @todo - are we not using videojs properly?
// @see - https://github.com/videojs/video.js/issues/5233
// @see - https://jsfiddle.net/karstenlh/96hrzp5w/
// This is currently needed for autoplay.
player.on('ready', () => {
this.logger.debug('the player is ready');
if (this.autoplayEnabled) {
// Even though the "ready" event has fired, it's not actually ready
// until the "next tick"...
setTimeout(() => {
player.play();
});
}
});
// @todo - this seems like we aren't using videojs properly
player.on('error', async (event) => {
this.logger.debug('the player encountered an error');
const retry = async () => {
this.logger.debug('retrying due to error');
if (this.options.maxRetriesOnError === 0) {
return;
}
if (this.options.maxRetriesOnError < 0 || player._errorRetriesCount <= this.options.maxRetriesOnError) {
// @todo - when can we reset this to zero?
player._errorRetriesCount++;
this.resetErrors(player);
const iov = this.getIov();
// @todo - investigate how this can be called when the iov has been destroyed
if (!iov || iov.destroyed) {
await this.initializeIov(player);
}
else {
await iov.restart();
}
}
};
const error = player.error();
switch (error.code) {
// timeout error
case -2: {
return retry();
}
case 0:
case 4:
case 5:
case 'PLAYER_ERR_Iov': {
break;
}
default: {
return retry();
}
}
});
player.on('play', async () => {
this.logger.debug('on player play event');
// @todo - it is probably unnecessary to have to completely tear down the
// existing iov and create a new one. But for now, this works
await this.initializeIov(player);
});
// the "pause" event gets triggered for some reason in scenarios where I do
// not expect it to be triggered. Therefore, we will create our own "stop"
// event to be able to better control the player to stop.
player.on('stop', () => {
this.logger.debug('on player stop event');
this.player.pause();
this.getIov().stop();
});
player.on('dispose', () => {
this.logger.debug('on dispose stop event');
this.destroy(player);
});
const {
visibilityChangeEventName,
} = utils.windowStateNames;
if (visibilityChangeEventName) {
document.addEventListener(
visibilityChangeEventName,
this.onVisibilityChange,
false,
);
}
}
onVisibilityChange = () => {
this.logger.debug('tab visibility changed...');
if (document[utils.windowStateNames.hiddenStateName]) {
// Continue to update the time, which will prevent videojs-errors from
// issuing a timeout error
this.visibilityChangeInterval = setInterval(async () => {
this.logger.debug('updating time...');
this.player.trigger('timeupdate');
}, 2000);
return;
}
if (this.visibilityChangeInterval) {
clearInterval(this.visibilityChangeInterval);
}
};
getVideojsErrorsOptions () {
this.logger.debug('getting videojs errors options...');
return {
timeout: 120 * 1000,
errors: {
PLAYER_ERR_NOT_COMPAT: {
type: 'PLAYER_ERR_NOT_COMPAT',
headline: 'This browser is unsupported.',
message: `Chrome ${utils.MINIMUM_CHROME_VERSION} or greater is required.`,
},
},
...this.options.videojsErrorsOptions,
};
}
resetErrors (player) {
this.logger.debug('resetting errors...');
// @see - https://github.com/videojs/video.js/issues/4401
player.error(null);
player.errorDisplay.close();
// Support for the videojs-errors library
// After an error occurs, and then we clear the error and its message
// above, we must re-enable videojs-errors on the player
if (player.errors) {
player.errors(this.getVideojsErrorsOptions());
}
}
getClspHandler (player = this.player) {
this.logger.debug('getting CLSP handler Iov...');
return player.tech(true).clsp;
}
getIov () {
this.logger.debug('getting Iov...');
return this.getClspHandler().iov;
}
onClspHandlerError = () => {
this.logger.debug('handling CLSP error...');
const clspHandler = this.getClspHandler();
clspHandler.destroy();
this.player.error({
// @todo - change the code to 'INSUFFICIENT_RESOURCES'
code: 0,
type: 'INSUFFICIENT_RESOURCES',
headline: 'Insufficient Resources',
message: 'The current hardware cannot support the current number of playing streams.',
});
};
async initializeIov (player) {
this.logger.debug('initializing Iov...');
const clspHandler = this.getClspHandler();
if (!clspHandler) {
throw new Error(`VideoJS Player ${player.id()} does not have CLSP tech!`);
}
clspHandler.off('error', this.onClspHandlerError);
clspHandler.on('error', this.onClspHandlerError);
await clspHandler.createIov(player);
const iov = this.getIov();
iov.ENABLE_METRICS = this.options.enableMetrics;
this.logger.debug('resgistering "firstFrameShown" event');
// @todo - is this still the correct way to track this? we may want to use
// the onShown handler in the iov instead
iov.on('firstFrameShown', () => {
this.logger.debug('about to trigger "firstFrameShown" event on videojs player');
player.trigger('firstFrameShown');
});
await iov.stop();
await iov.changeSrc(clspHandler.source_.src).firstFrameReceivedPromise;
}
destroy (player = this.player) {
this.logger.debug('destroying...');
// Note that when the 'dispose' event is fired, this.player no longer exists
if (!player) {
this.logger.warn('Unable to destroy CLSP VideoJS Plugin without the player!');
return;
}
if (this.destroyed) {
this.logger.debug('Tried to destroy when already destroyed');
return;
}
this.destroyed = true;
// @todo - destroy the tech, since it is a player-specific instance
try {
const clspHandler = this.getClspHandler(player);
clspHandler.destroy();
clspHandler.off('error', this.onClspHandlerError);
const {
visibilityChangeEventName,
} = utils.windowStateNames;
if (visibilityChangeEventName) {
this.logger.debug('removing onVisibilityChange listener...');
document.removeEventListener(visibilityChangeEventName, this.onVisibilityChange);
}
if (this.visibilityChangeInterval) {
this.logger.debug('removing visibilityChangeInterval...');
clearInterval(this.visibilityChangeInterval);
}
this._playerOptions = null;
this.currentSourceIndex = null;
}
catch (error) {
// @todo - need to improve iov destroy logic...
this.logger.error('Error while destroying CLSP VideoJS Plugin instance!');
this.logger.error(error);
}
}
};