videojs-per-source-behaviors
Version:
A Video.js plugin for enhancing a player with behaviors related to changing media sources.
320 lines (272 loc) • 9.35 kB
JavaScript
/*! @name videojs-per-source-behaviors @version 3.0.1 @license Apache-2.0 */
import videojs from 'video.js';
var version = "3.0.1";
const Html5 = videojs.getTech('Html5'); // Video.js 5/6 cross-compatibility.
const registerPlugin = videojs.registerPlugin || videojs.plugin;
/**
* For the most part, these are the events that occur early in the lifecycle
* of a source, but there is considerable variability across browsers and
* devices (not to mention properties like autoplay and preload). As such, we
* listen to a bunch of events for source changes.
*
* @type {Array}
*/
const CHANGE_DETECT_EVENTS = ['abort', 'emptied', 'loadstart', 'play'];
/**
* These events will indicate that the source is "unstable" (i.e. it might be
* about to change).
*
* @type {Array}
*/
const UNSTABLE_EVENTS = ['abort', 'emptied'];
/**
* These are the ad loading and playback states we care about from
* contrib-ads 5.
*
* @type {Array}
*/
const AD_STATES = ['ad-playback', 'ads-ready?', 'postroll?', 'preroll?'];
/**
* Whether or not a string represents an "ad state".
*
* @param {string} s
* The string to check
*
* @return {boolean}
* If the string is an ad state
*/
const isAdState = s => AD_STATES.indexOf(s) > -1;
/**
* Determine whether or not a player is using contrib-ads 6+.
*
* @param {Player} p
* The player
*
* @return {boolean}
* if the player is using contrib-ads 6+
*/
const usingAds6Plus = p => p.usingPlugin('ads') && typeof p.ads.inAdBreak === 'function';
/**
* Whether or not the player should ignore an event due to ad states.
*
* @param {Player} p
* The player
*
* @param {Event} e
* An event object
*
* @return {boolean}
* If the player is in an ad state
*/
const ignoreEvent = (p, e) => {
// No ads, no ad states to ignore!
if (!p.usingPlugin('ads')) {
return false;
} // In contrib-ads 6+ we only care about non-loadstart events because
// loadstart events _may_ happen during ad mode.
if (usingAds6Plus(p)) {
return p.ads.isInAdMode() && e.type !== 'loadstart';
} // contrib-ads 5+
return isAdState(p.ads.state);
};
let psbGuid = 0;
/**
* Applies per-source behaviors to a video.js Player object.
*/
const perSourceBehaviors = function () {
const perSrcListeners = [];
let cachedSrc;
let disabled = false;
let srcChangeTimer;
let srcStable = true;
/**
* Creates an event binder function of a given type.
*
* @param {boolean} isOne
* Rather than delegating to the player's `one()` method, we want to
* retain full control over when the listener is unbound (particularly
* due to the ability for per-source behaviors to be toggled on and
* off at will).
*
* @return {Function}
* the per source binder function
*/
const createPerSrcBinder = isOne => {
return function (first, second) {
// Do not bind new listeners when per-source behaviors are disabled.
if (this.perSourceBehaviors.disabled()) {
return;
}
const isTargetPlayer = arguments.length === 2;
const originalSrc = this.currentSrc(); // This array is the set of arguments to use for `on()` and `off()` methods
// of the player.
const args = [first]; // Make sure we bind here so that a GUID is set on the original listener
// and that it is bound to the proper context.
const passedListener = arguments[arguments.length - 1];
const listenerContext = isTargetPlayer ? this : first;
const originalListener = passedListener.bind(listenerContext); // The wrapped listener `subargs` are the arguments passed to the original
// listener (i.e. the Event object and an additional data hash).
const wrappedListener = (...subargs) => {
const changed = this.currentSrc() !== originalSrc; // Do not evaluate listeners if per-source behaviors are disabled.
if (this.perSourceBehaviors.disabled()) {
return;
}
if (changed || isOne) {
this.off(...args);
}
if (!changed) {
originalListener(...subargs);
}
}; // Make sure the wrapped listener and the original listener share a GUID,
// so that video.js properly removes event bindings when `off()` is passed
// the original listener!
//
// This property is used internally by Video.js for tracking bound
// listeners in its event system. Normally, Video.js creates this `guid`
// through its `bind` method, but that method is deprecated in public
// interfaces.
//
// Basically, this allows us to avoid deprecation warnings from calling
// `videojs.bind` when used with Video.js 8.
wrappedListener.guid = originalListener.guid = passedListener.guid = `psb-${++psbGuid}`; // If we are targeting a different object from the player, we need to include
// the second argument.
if (!isTargetPlayer) {
args.push(second);
}
args.push(wrappedListener);
perSrcListeners.push(args);
return this.on(...args);
};
};
this.perSourceBehaviors = {
/**
* Disable per-source behaviors on this player.
*
* @return {boolean}
*/
disable: function disable() {
this.clearTimeout(srcChangeTimer);
srcChangeTimer = null;
disabled = true;
return disabled;
}.bind(this),
/**
* Whether per-source behaviors are disabled on this player.
*
* @return {boolean}
* if the per-source behaviors are disabled
*/
disabled() {
return disabled;
},
/**
* Enable per-source behaviors on this player.
*
* @return {boolean}
* always returns true
*/
enable() {
disabled = false;
return disabled;
},
/**
* Whether per-source behaviors are disabled on this player.
*
* @return {boolean}
* if the per-source behaviors are enabled
*/
enabled() {
return !disabled;
},
/**
* Whether or not the source is "stable". This will return `true` if the
* plugin feels that we may be about to change sources.
*
* @return {boolean}
* Whether the source is stable or not
*/
isSrcStable() {
return srcStable;
},
VERSION: version
};
/**
* Bind an event listener on a per-source basis.
*
* @function onPerSrc
* @param {String|Array|Component|Element} first
* The event type(s) or target Component or Element.
*
* @param {Function|String|Array} second
* The event listener or event type(s) (when `first` is target).
*
* @param {Function} third
* The event listener (when `second` is event type(s)).
*
* @return {Player}
*/
this.onPerSrc = createPerSrcBinder();
/**
* Bind an event listener on a per-source basis. This listener can only
* be called once.
*
* @function onePerSrc
* @param {String|Array|Component|Element} first
* The event type(s) or target Component or Element.
*
* @param {Function|String|Array} second
* The event listener or event type(s) (when `first` is target).
*
* @param {Function} third
* The event listener (when `second` is event type(s)).
*
* @return {Player}
*/
this.onePerSrc = createPerSrcBinder(true); // Clear out perSrcListeners cache on player dispose.
this.on('dispose', () => {
perSrcListeners.length = 0;
});
this.on(CHANGE_DETECT_EVENTS, e => {
// Bail-out conditions.
if (this.perSourceBehaviors.disabled() || srcChangeTimer || ignoreEvent(this, e)) {
return;
} // If we did not previously detect that we were in an unstable state and
// this was an event that qualifies as unstable, do that now. In the future,
// we may want to restrict the conditions under which this is triggered by
// checking networkState and/or readyState for reasonable values such as
// NETWORK_NO_SOURCE and HAVE_NOTHING.
if (srcStable && UNSTABLE_EVENTS.indexOf(e.type) > -1) {
srcStable = false;
this.trigger('sourceunstable');
} // Track any and all interim events from this one until the next tick
// when we evaluate the timer.
const interimEvents = [];
const addInterimEvent = f => interimEvents.push({
time: Date.now(),
event: f
});
addInterimEvent(e);
this.on(Html5.Events, addInterimEvent);
srcChangeTimer = this.setTimeout(() => {
const currentSrc = this.currentSrc();
srcStable = true;
srcChangeTimer = null;
this.off(Html5.Events, addInterimEvent);
if (currentSrc && currentSrc !== cachedSrc) {
// Remove per-source listeners explicitly when we know the source has
// changed before we trigger the "sourcechanged" listener.
perSrcListeners.forEach(args => this.off(...args));
perSrcListeners.length = 0;
this.trigger('sourcechanged', {
interimEvents,
from: cachedSrc,
to: currentSrc
});
cachedSrc = currentSrc;
}
}, 1);
});
};
perSourceBehaviors.VERSION = version;
registerPlugin('perSourceBehaviors', perSourceBehaviors);
export { perSourceBehaviors as default };