/**
* @file middleware.js
* @module middleware
*/
import {toTitleCase} from '../utils/str.js';
const middlewares = {};
const middlewareInstances = {};
export const TERMINATOR = {};
/**
* A middleware object is a plain JavaScript object that has methods that
* match the {@link Tech} methods found in the lists of allowed
* {@link module:middleware.allowedGetters|getters},
* {@link module:middleware.allowedSetters|setters}, and
* {@link module:middleware.allowedMediators|mediators}.
*
* @typedef {Object} MiddlewareObject
*/
/**
* A middleware factory function that should return a
* {@link module:middleware~MiddlewareObject|MiddlewareObject}.
*
* This factory will be called for each player when needed, with the player
* passed in as an argument.
*
* @callback MiddlewareFactory
* @param { import('../player').default } player
* A Video.js player.
*/
/**
* Define a middleware that the player should use by way of a factory function
* that returns a middleware object.
*
* @param {string} type
* The MIME type to match or `"*"` for all MIME types.
*
* @param {MiddlewareFactory} middleware
* A middleware factory function that will be executed for
* matching types.
*/
export function use(type, middleware) {
middlewares[type] = middlewares[type] || [];
middlewares[type].push(middleware);
}
/**
* Gets middlewares by type (or all middlewares).
*
* @param {string} type
* The MIME type to match or `"*"` for all MIME types.
*
* @return {Function[]|undefined}
* An array of middlewares or `undefined` if none exist.
*/
export function getMiddleware(type) {
if (type) {
return middlewares[type];
}
return middlewares;
}
/**
* Asynchronously sets a source using middleware by recursing through any
* matching middlewares and calling `setSource` on each, passing along the
* previous returned value each time.
*
* @param { import('../player').default } player
* A {@link Player} instance.
*
* @param {Tech~SourceObject} src
* A source object.
*
* @param {Function}
* The next middleware to run.
*/
export function setSource(player, src, next) {
player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
}
/**
* When the tech is set, passes the tech to each middleware's `setTech` method.
*
* @param {Object[]} middleware
* An array of middleware instances.
*
* @param { import('../tech/tech').default } tech
* A Video.js tech.
*/
export function setTech(middleware, tech) {
middleware.forEach((mw) => mw.setTech && mw.setTech(tech));
}
/**
* Calls a getter on the tech first, through each middleware
* from right to left to the player.
*
* @param {Object[]} middleware
* An array of middleware instances.
*
* @param { import('../tech/tech').default } tech
* The current tech.
*
* @param {string} method
* A method name.
*
* @return {*}
* The final value from the tech after middleware has intercepted it.
*/
export function get(middleware, tech, method) {
return middleware.reduceRight(middlewareIterator(method), tech[method]());
}
/**
* Takes the argument given to the player and calls the setter method on each
* middleware from left to right to the tech.
*
* @param {Object[]} middleware
* An array of middleware instances.
*
* @param { import('../tech/tech').default } tech
* The current tech.
*
* @param {string} method
* A method name.
*
* @param {*} arg
* The value to set on the tech.
*
* @return {*}
* The return value of the `method` of the `tech`.
*/
export function set(middleware, tech, method, arg) {
return tech[method](middleware.reduce(middlewareIterator(method), arg));
}
/**
* Takes the argument given to the player and calls the `call` version of the
* method on each middleware from left to right.
*
* Then, call the passed in method on the tech and return the result unchanged
* back to the player, through middleware, this time from right to left.
*
* @param {Object[]} middleware
* An array of middleware instances.
*
* @param { import('../tech/tech').default } tech
* The current tech.
*
* @param {string} method
* A method name.
*
* @param {*} arg
* The value to set on the tech.
*
* @return {*}
* The return value of the `method` of the `tech`, regardless of the
* return values of middlewares.
*/
export function mediate(middleware, tech, method, arg = null) {
const callMethod = 'call' + toTitleCase(method);
const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
const terminated = middlewareValue === TERMINATOR;
// deprecated. The `null` return value should instead return TERMINATOR to
// prevent confusion if a techs method actually returns null.
const returnValue = terminated ? null : tech[method](middlewareValue);
executeRight(middleware, method, returnValue, terminated);
return returnValue;
}
/**
* Enumeration of allowed getters where the keys are method names.
*
* @type {Object}
*/
export const allowedGetters = {
buffered: 1,
currentTime: 1,
duration: 1,
muted: 1,
played: 1,
paused: 1,
seekable: 1,
volume: 1,
ended: 1
};
/**
* Enumeration of allowed setters where the keys are method names.
*
* @type {Object}
*/
export const allowedSetters = {
setCurrentTime: 1,
setMuted: 1,
setVolume: 1
};
/**
* Enumeration of allowed mediators where the keys are method names.
*
* @type {Object}
*/
export const allowedMediators = {
play: 1,
pause: 1
};
function middlewareIterator(method) {
return (value, mw) => {
// if the previous middleware terminated, pass along the termination
if (value === TERMINATOR) {
return TERMINATOR;
}
if (mw[method]) {
return mw[method](value);
}
return value;
};
}
function executeRight(mws, method, value, terminated) {
for (let i = mws.length - 1; i >= 0; i--) {
const mw = mws[i];
if (mw[method]) {
mw[method](terminated, value);
}
}
}
/**
* Clear the middleware cache for a player.
*
* @param { import('../player').default } player
* A {@link Player} instance.
*/
export function clearCacheForPlayer(player) {
middlewareInstances[player.id()] = null;
}
/**
* {
* [playerId]: [[mwFactory, mwInstance], ...]
* }
*
* @private
*/
function getOrCreateFactory(player, mwFactory) {
const mws = middlewareInstances[player.id()];
let mw = null;
if (mws === undefined || mws === null) {
mw = mwFactory(player);
middlewareInstances[player.id()] = [[mwFactory, mw]];
return mw;
}
for (let i = 0; i < mws.length; i++) {
const [mwf, mwi] = mws[i];
if (mwf !== mwFactory) {
continue;
}
mw = mwi;
}
if (mw === null) {
mw = mwFactory(player);
mws.push([mwFactory, mw]);
}
return mw;
}
function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
const [mwFactory, ...mwrest] = middleware;
// if mwFactory is a string, then we're at a fork in the road
if (typeof mwFactory === 'string') {
setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
// if we have an mwFactory, call it with the player to get the mw,
// then call the mw's setSource method
} else if (mwFactory) {
const mw = getOrCreateFactory(player, mwFactory);
// if setSource isn't present, implicitly select this middleware
if (!mw.setSource) {
acc.push(mw);
return setSourceHelper(src, mwrest, next, player, acc, lastRun);
}
mw.setSource(Object.assign({}, src), function(err, _src) {
// something happened, try the next middleware on the current level
// make sure to use the old src
if (err) {
return setSourceHelper(src, mwrest, next, player, acc, lastRun);
}
// we've succeeded, now we need to go deeper
acc.push(mw);
// if it's the same type, continue down the current chain
// otherwise, we want to go down the new chain
setSourceHelper(
_src,
src.type === _src.type ? mwrest : middlewares[_src.type],
next,
player,
acc,
lastRun
);
});
} else if (mwrest.length) {
setSourceHelper(src, mwrest, next, player, acc, lastRun);
} else if (lastRun) {
next(src, acc);
} else {
setSourceHelper(src, middlewares['*'], next, player, acc, true);
}
}