videojs-contrib-ads
Version:
A framework that provides common functionality needed by video advertisement libraries working with video.js.
1,348 lines (1,239 loc) • 94.5 kB
JavaScript
/*! @name videojs-contrib-ads @version 7.1.0 @license Apache-2.0 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js'), require('global/window'), require('global/document')) :
typeof define === 'function' && define.amd ? define(['video.js', 'global/window', 'global/document'], factory) :
(global = global || self, global.videojsContribAds = factory(global.videojs, global.window, global.document));
}(this, function (videojs, window, document) { 'use strict';
videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
window = window && window.hasOwnProperty('default') ? window['default'] : window;
document = document && document.hasOwnProperty('default') ? document['default'] : document;
var version = "7.1.0";
/*
* Implements the public API available in `player.ads` as well as application state.
*/
function getAds(player) {
return {
disableNextSnapshotRestore: false,
// This is true if we have finished actual content playback but haven't
// dealt with postrolls and officially ended yet
_contentEnding: false,
// This is set to true if the content has officially ended at least once.
// After that, the user can seek backwards and replay content, but _contentHasEnded
// remains true.
_contentHasEnded: false,
// Tracks if loadstart has happened yet for the initial source. It is not reset
// on source changes because loadstart is the event that signals to the ad plugin
// that the source has changed. Therefore, no special signaling is needed to know
// that there has been one for subsequent sources.
_hasThereBeenALoadStartDuringPlayerLife: false,
// Tracks if loadeddata has happened yet for the current source.
_hasThereBeenALoadedData: false,
// Tracks if loadedmetadata has happened yet for the current source.
_hasThereBeenALoadedMetaData: false,
// Are we after startLinearAdMode and before endLinearAdMode?
_inLinearAdMode: false,
// Should we block calls to play on the content player?
_shouldBlockPlay: false,
// Was play blocked by the plugin's playMiddleware feature?
_playBlocked: false,
// Tracks whether play has been requested for this source,
// either by the play method or user interaction
_playRequested: false,
// This is an estimation of the current ad type being played
// This is experimental currently. Do not rely on its presence or behavior!
adType: null,
VERSION: version,
reset: function reset() {
player.ads.disableNextSnapshotRestore = false;
player.ads._contentEnding = false;
player.ads._contentHasEnded = false;
player.ads.snapshot = null;
player.ads.adType = null;
player.ads._hasThereBeenALoadedData = false;
player.ads._hasThereBeenALoadedMetaData = false;
player.ads._cancelledPlay = false;
player.ads._shouldBlockPlay = false;
player.ads._playBlocked = false;
player.ads.nopreroll_ = false;
player.ads.nopostroll_ = false;
player.ads._playRequested = false;
},
// Call this when an ad response has been received and there are
// linear ads ready to be played.
startLinearAdMode: function startLinearAdMode() {
player.ads._state.startLinearAdMode();
},
// Call this when a linear ad pod has finished playing.
endLinearAdMode: function endLinearAdMode() {
player.ads._state.endLinearAdMode();
},
// Call this when an ad response has been received but there are no
// linear ads to be played (i.e. no ads available, or overlays).
// This has no effect if we are already in an ad break. Always
// use endLinearAdMode() to exit from linear ad-playback state.
skipLinearAdMode: function skipLinearAdMode() {
player.ads._state.skipLinearAdMode();
},
// With no arguments, returns a boolean value indicating whether or not
// contrib-ads is set to treat ads as stitched with content in a single
// stream. With arguments, treated as a setter, but this behavior is
// deprecated.
stitchedAds: function stitchedAds(arg) {
if (arg !== undefined) {
videojs.log.warn('Using player.ads.stitchedAds() as a setter is deprecated, ' + 'it should be set as an option upon initialization of contrib-ads.');
// Keep the private property and the settings in sync. When this
// setter is removed, we can probably stop using the private property.
this.settings.stitchedAds = !!arg;
}
return this.settings.stitchedAds;
},
// Returns whether the video element has been modified since the
// snapshot was taken.
// We test both src and currentSrc because changing the src attribute to a URL that
// AdBlocker is intercepting doesn't update currentSrc.
videoElementRecycled: function videoElementRecycled() {
if (player.ads.shouldPlayContentBehindAd(player)) {
return false;
}
if (!this.snapshot) {
throw new Error('You cannot use videoElementRecycled while there is no snapshot.');
}
var srcChanged = player.tech_.src() !== this.snapshot.src;
var currentSrcChanged = player.currentSrc() !== this.snapshot.currentSrc;
return srcChanged || currentSrcChanged;
},
// Returns a boolean indicating if given player is in live mode.
// One reason for this: https://github.com/videojs/video.js/issues/3262
// Also, some live content can have a duration.
isLive: function isLive(somePlayer) {
if (somePlayer === void 0) {
somePlayer = player;
}
if (typeof somePlayer.ads.settings.contentIsLive === 'boolean') {
return somePlayer.ads.settings.contentIsLive;
} else if (somePlayer.duration() === Infinity) {
return true;
} else if (videojs.browser.IOS_VERSION === '8' && somePlayer.duration() === 0) {
return true;
}
return false;
},
// Return true if content playback should mute and continue during ad breaks.
// This is only done during live streams on platforms where it's supported.
// This improves speed and accuracy when returning from an ad break.
shouldPlayContentBehindAd: function shouldPlayContentBehindAd(somePlayer) {
if (somePlayer === void 0) {
somePlayer = player;
}
if (!somePlayer) {
throw new Error('shouldPlayContentBehindAd requires a player as a param');
} else if (!somePlayer.ads.settings.liveCuePoints) {
return false;
} else {
return !videojs.browser.IS_IOS && !videojs.browser.IS_ANDROID && somePlayer.duration() === Infinity;
}
},
// Return true if the ads plugin should save and restore snapshots of the
// player state when moving into and out of ad mode.
shouldTakeSnapshots: function shouldTakeSnapshots(somePlayer) {
if (somePlayer === void 0) {
somePlayer = player;
}
return !this.shouldPlayContentBehindAd(somePlayer) && !this.stitchedAds();
},
// Returns true if player is in ad mode.
//
// Ad mode definition:
// If content playback is blocked by the ad plugin.
//
// Examples of ad mode:
//
// * Waiting to find out if an ad is going to play while content would normally be
// playing.
// * Waiting for an ad to start playing while content would normally be playing.
// * An ad is playing (even if content is also playing)
// * An ad has completed and content is about to resume, but content has not resumed
// yet.
//
// Examples of not ad mode:
//
// * Content playback has not been requested
// * Content playback is paused
// * An asynchronous ad request is ongoing while content is playing
// * A non-linear ad is active
isInAdMode: function isInAdMode() {
return this._state.isAdState();
},
// Returns true if in ad mode but an ad break hasn't started yet.
isWaitingForAdBreak: function isWaitingForAdBreak() {
return this._state.isWaitingForAdBreak();
},
// Returns true if content is resuming after an ad. This is part of ad mode.
isContentResuming: function isContentResuming() {
return this._state.isContentResuming();
},
// Deprecated because the name was misleading. Use inAdBreak instead.
isAdPlaying: function isAdPlaying() {
return this._state.inAdBreak();
},
// Returns true if an ad break is ongoing. This is part of ad mode.
// An ad break is the time between startLinearAdMode and endLinearAdMode.
inAdBreak: function inAdBreak() {
return this._state.inAdBreak();
},
/*
* Remove the poster attribute from the video element tech, if present. When
* reusing a video element for multiple videos, the poster image will briefly
* reappear while the new source loads. Removing the attribute ahead of time
* prevents the poster from showing up between videos.
*
* @param {Object} player The videojs player object
*/
removeNativePoster: function removeNativePoster() {
var tech = player.$('.vjs-tech');
if (tech) {
tech.removeAttribute('poster');
}
},
debug: function debug() {
if (this.settings.debug) {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
if (args.length === 1 && typeof args[0] === 'string') {
videojs.log('ADS: ' + args[0]);
} else {
videojs.log.apply(videojs, ['ADS:'].concat(args));
}
}
}
};
}
/*
The goal of this feature is to make player events work as an integrator would
expect despite the presense of ads. For example, an integrator would expect
an `ended` event to happen once the content is ended. If an `ended` event is sent
as a result of a preroll ending, that is a bug. The `redispatch` method should recognize
such `ended` events and prefix them so they are sent as `adended`, and so on with
all other player events.
*/
// Cancel an event.
// Video.js wraps native events. This technique stops propagation for the Video.js event
// (AKA player event or wrapper event) while native events continue propagating.
var cancelEvent = function cancelEvent(player, event) {
event.isImmediatePropagationStopped = function () {
return true;
};
event.cancelBubble = true;
event.isPropagationStopped = function () {
return true;
};
};
// Redispatch an event with a prefix.
// Cancels the event, then sends a new event with the type of the original
// event with the given prefix added.
// The inclusion of the "state" property should be removed in a future
// major version update with instructions to migrate any code that relies on it.
// It is an implementation detail and relying on it creates fragility.
var prefixEvent = function prefixEvent(player, prefix, event) {
cancelEvent(player, event);
player.trigger({
type: prefix + event.type,
originalEvent: event
});
};
// Playing event
// Requirements:
// * Normal playing event when there is no preroll
// * No playing event before preroll
// * At least one playing event after preroll
var handlePlaying = function handlePlaying(player, event) {
if (player.ads.isInAdMode()) {
if (player.ads.isContentResuming()) {
// Prefix playing event when switching back to content after postroll.
if (player.ads._contentEnding) {
prefixEvent(player, 'content', event);
}
// Prefix all other playing events during ads.
} else {
prefixEvent(player, 'ad', event);
}
}
};
// Ended event
// Requirements:
// * A single ended event when there is no postroll
// * No ended event before postroll
// * A single ended event after postroll
var handleEnded = function handleEnded(player, event) {
if (player.ads.isInAdMode()) {
// Cancel ended events during content resuming. Normally we would
// prefix them, but `contentended` has a special meaning. In the
// future we'd like to rename the existing `contentended` to
// `readyforpostroll`, then we could remove the special `resumeended`
// and do a conventional content prefix here.
if (player.ads.isContentResuming()) {
cancelEvent(player, event);
// Important: do not use this event outside of videojs-contrib-ads.
// It will be removed and your code will break.
// Ideally this would simply be `contentended`, but until
// `contentended` no longer has a special meaning it cannot be
// changed.
player.trigger('resumeended');
// Ad prefix in ad mode
} else {
prefixEvent(player, 'ad', event);
}
// Prefix ended due to content ending before postroll check
} else if (!player.ads._contentHasEnded && !player.ads.stitchedAds()) {
// This will change to cancelEvent after the contentended deprecation
// period (contrib-ads 7)
prefixEvent(player, 'content', event);
// Content ended for the first time, time to check for postrolls
player.trigger('readyforpostroll');
}
};
// handleLoadEvent is used for loadstart, loadeddata, and loadedmetadata
// Requirements:
// * Initial event is not prefixed
// * Event due to ad loading is prefixed
// * Event due to content source change is not prefixed
// * Event due to content resuming is prefixed
var handleLoadEvent = function handleLoadEvent(player, event) {
// Initial event
if (event.type === 'loadstart' && !player.ads._hasThereBeenALoadStartDuringPlayerLife || event.type === 'loadeddata' && !player.ads._hasThereBeenALoadedData || event.type === 'loadedmetadata' && !player.ads._hasThereBeenALoadedMetaData) {
return;
// Ad playing
} else if (player.ads.inAdBreak()) {
prefixEvent(player, 'ad', event);
// Source change
} else if (player.currentSrc() !== player.ads.contentSrc) {
return;
// Content resuming
} else {
prefixEvent(player, 'content', event);
}
};
// Play event
// Requirements:
// * Play events have the "ad" prefix when an ad is playing
// * Play events have the "content" prefix when content is resuming
// Play requests are unique because they represent user intention to play. They happen
// because the user clicked play, or someone called player.play(), etc. It could happen
// multiple times during ad loading, regardless of where we are in the process. With our
// current architecture, this could cause the content to start playing.
// Therefore, contrib-ads must always either:
// - cancelContentPlay if there is any possible chance the play caused the
// content to start playing, even if we are technically in ad mode. In order for
// that to happen, play events need to be unprefixed until the last possible moment.
// - use playMiddleware to stop the play from reaching the Tech so there is no risk
// of the content starting to play.
// Currently, playMiddleware is only supported on desktop browsers with
// video.js after version 6.7.1.
var handlePlay = function handlePlay(player, event) {
if (player.ads.inAdBreak()) {
prefixEvent(player, 'ad', event);
// Content resuming
} else if (player.ads.isContentResuming()) {
prefixEvent(player, 'content', event);
}
};
// Handle a player event, either by redispatching it with a prefix, or by
// letting it go on its way without any meddling.
function redispatch(event) {
// Events with special treatment
if (event.type === 'playing') {
handlePlaying(this, event);
} else if (event.type === 'ended') {
handleEnded(this, event);
} else if (event.type === 'loadstart' || event.type === 'loadeddata' || event.type === 'loadedmetadata') {
handleLoadEvent(this, event);
} else if (event.type === 'play') {
handlePlay(this, event);
// Standard handling for all other events
} else if (this.ads.isInAdMode()) {
if (this.ads.isContentResuming()) {
// Event came from snapshot restore after an ad, use "content" prefix
prefixEvent(this, 'content', event);
} else {
// Event came from ad playback, use "ad" prefix
prefixEvent(this, 'ad', event);
}
}
}
/*
This feature sends a `contentupdate` event when the player source changes.
*/
// Start sending contentupdate events
function initializeContentupdate(player) {
// Keep track of the current content source
// If you want to change the src of the video without triggering
// the ad workflow to restart, you can update this variable before
// modifying the player's source
player.ads.contentSrc = player.currentSrc();
player.ads._seenInitialLoadstart = false;
// Check if a new src has been set, if so, trigger contentupdate
var checkSrc = function checkSrc() {
if (!player.ads.inAdBreak()) {
var src = player.currentSrc();
if (src !== player.ads.contentSrc) {
if (player.ads._seenInitialLoadstart) {
player.trigger({
type: 'contentchanged'
});
}
player.trigger({
type: 'contentupdate',
oldValue: player.ads.contentSrc,
newValue: src
});
player.ads.contentSrc = src;
}
player.ads._seenInitialLoadstart = true;
}
};
// loadstart reliably indicates a new src has been set
player.on('loadstart', checkSrc);
}
/**
* Current tcfData returned from CMP
* Updated on event listener rather than having to make an asyc
* check within the macro resolver
*/
var tcData = {};
/**
* Sets up a proxy for the TCF API in an iframed player, if a parent frame
* that has implemented the TCF API is detected
* https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#is-there-a-sample-iframe-script-call-to-the-cmp-api
*/
var proxyTcfApi = function proxyTcfApi(_) {
if (videojs.dom.isInFrame() && typeof window.__tcfapi !== 'function') {
var frame = window;
var cmpFrame;
var cmpCallbacks = {};
while (frame) {
try {
if (frame.frames.__tcfapiLocator) {
cmpFrame = frame;
break;
}
} catch (ignore) {
// empty
}
if (frame === window.top) {
break;
}
frame = frame.parent;
}
if (!cmpFrame) {
return;
}
window.__tcfapi = function (cmd, version, callback, arg) {
var callId = Math.random() + '';
var msg = {
__tcfapiCall: {
command: cmd,
parameter: arg,
version: version,
callId: callId
}
};
cmpCallbacks[callId] = callback;
cmpFrame.postMessage(msg, '*');
};
window.addEventListener('message', function (event) {
var json = {};
try {
json = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
} catch (ignore) {
// empty
}
var payload = json.__tcfapiReturn;
if (payload) {
if (typeof cmpCallbacks[payload.callId] === 'function') {
cmpCallbacks[payload.callId](payload.returnValue, payload.success);
cmpCallbacks[payload.callId] = null;
}
}
}, false);
}
};
/**
* Sets up event listener for changes to consent data.
*/
var listenToTcf = function listenToTcf() {
proxyTcfApi();
if (typeof window.__tcfapi === 'function') {
window.__tcfapi('addEventListener', 2, function (data, success) {
if (success) {
tcData = data;
}
});
}
};
/*
This feature provides an optional method for ad plugins to insert run-time values
into an ad server URL or configuration.
*/
// Return URI encoded version of value if uriEncode is true
var uriEncodeIfNeeded = function uriEncodeIfNeeded(value, uriEncode) {
if (uriEncode) {
return encodeURIComponent(value);
}
return value;
};
// Add custom field macros to macros object
// based on given name for custom fields property of mediainfo object.
var customFields = function customFields(mediainfo, macros, customFieldsName) {
if (mediainfo && mediainfo[customFieldsName]) {
var fields = mediainfo[customFieldsName];
var fieldNames = Object.keys(fields);
for (var i = 0; i < fieldNames.length; i++) {
var tag = '{mediainfo.' + customFieldsName + '.' + fieldNames[i] + '}';
macros[tag] = fields[fieldNames[i]];
}
}
};
// Public method that ad plugins use for ad macros.
// "string" is any string with macros to be replaced
// "uriEncode" if true will uri encode macro values when replaced
// "customMacros" is a object with custom macros and values to map them to
// - For example: {'{five}': 5}
// Return value is is "string" with macros replaced
// - For example: adMacroReplacement('{player.id}') returns a string of the player id
function adMacroReplacement(string, uriEncode, customMacros) {
var _this = this;
var defaults = {};
// Get macros with defaults e.g. {x=y}, store values and replace with standard macros
string = string.replace(/{([^}=]+)=([^}]*)}/g, function (match, name, defaultVal) {
defaults["{" + name + "}"] = defaultVal;
return "{" + name + "}";
});
if (uriEncode === undefined) {
uriEncode = false;
}
var macros = {};
if (customMacros !== undefined) {
macros = customMacros;
}
// Static macros
macros['{player.id}'] = this.options_['data-player'] || this.id_;
macros['{player.height}'] = this.currentHeight();
macros['{player.width}'] = this.currentWidth();
macros['{player.heightInt}'] = Math.round(this.currentHeight());
macros['{player.widthInt}'] = Math.round(this.currentWidth());
macros['{mediainfo.id}'] = this.mediainfo ? this.mediainfo.id : '';
macros['{mediainfo.name}'] = this.mediainfo ? this.mediainfo.name : '';
macros['{mediainfo.duration}'] = this.mediainfo ? this.mediainfo.duration : '';
macros['{player.duration}'] = this.duration();
macros['{player.durationInt}'] = Math.round(this.duration());
macros['{player.pageUrl}'] = videojs.dom.isInFrame() ? document.referrer : window.location.href;
macros['{playlistinfo.id}'] = this.playlistinfo ? this.playlistinfo.id : '';
macros['{playlistinfo.name}'] = this.playlistinfo ? this.playlistinfo.name : '';
macros['{timestamp}'] = new Date().getTime();
macros['{document.referrer}'] = document.referrer;
macros['{window.location.href}'] = window.location.href;
macros['{random}'] = Math.floor(Math.random() * 1000000000000);
['description', 'tags', 'reference_id', 'ad_keys'].forEach(function (prop) {
if (_this.mediainfo && _this.mediainfo[prop]) {
macros["{mediainfo." + prop + "}"] = _this.mediainfo[prop];
} else if (defaults["{mediainfo." + prop + "}"]) {
macros["{mediainfo." + prop + "}"] = defaults["{mediainfo." + prop + "}"];
} else {
macros["{mediainfo." + prop + "}"] = '';
}
});
// Custom fields in mediainfo
customFields(this.mediainfo, macros, 'custom_fields');
customFields(this.mediainfo, macros, 'customFields');
// tcf macros
Object.keys(tcData).forEach(function (key) {
macros["{tcf." + key + "}"] = tcData[key];
});
// Ad servers commonly want this bool as an int
macros['{tcf.gdprAppliesInt}'] = tcData.gdprApplies ? 1 : 0;
// Go through all the replacement macros and apply them to the string.
// This will replace all occurrences of the replacement macros.
for (var i in macros) {
string = string.split(i).join(uriEncodeIfNeeded(macros[i], uriEncode));
}
// Page variables
string = string.replace(/{pageVariable\.([^}]+)}/g, function (match, name) {
var value;
var context = window;
var names = name.split('.');
// Iterate down multiple levels of selector without using eval
// This makes things like pageVariable.foo.bar work
for (var _i = 0; _i < names.length; _i++) {
if (_i === names.length - 1) {
value = context[names[_i]];
} else {
context = context[names[_i]];
if (typeof context === 'undefined') {
break;
}
}
}
var type = typeof value;
// Only allow certain types of values. Anything else is probably a mistake.
if (value === null) {
return 'null';
} else if (value === undefined) {
if (defaults["{pageVariable." + name + "}"]) {
return defaults["{pageVariable." + name + "}"];
}
videojs.log.warn("Page variable \"" + name + "\" not found");
return '';
} else if (type !== 'string' && type !== 'number' && type !== 'boolean') {
videojs.log.warn("Page variable \"" + name + "\" is not a supported type");
return '';
}
return uriEncodeIfNeeded(String(value), uriEncode);
});
// Replace defaults
for (var defaultVal in defaults) {
string = string.replace(defaultVal, defaults[defaultVal]);
}
return string;
}
/*
* This feature allows metadata text tracks to be manipulated once available
* @see processMetadataTracks.
* It also allows ad implementations to leverage ad cues coming through
* text tracks, @see processAdTrack
**/
var cueTextTracks = {};
/*
* This feature allows metadata text tracks to be manipulated once they are available,
* usually after the 'loadstart' event is observed on the player
* @param player A reference to a player
* @param processMetadataTrack A callback that performs some operations on a
* metadata text track
**/
cueTextTracks.processMetadataTracks = function (player, processMetadataTrack) {
var tracks = player.textTracks();
var setModeAndProcess = function setModeAndProcess(track) {
if (track.kind === 'metadata') {
player.ads.cueTextTracks.setMetadataTrackMode(track);
processMetadataTrack(player, track);
}
};
// Text tracks are available
for (var i = 0; i < tracks.length; i++) {
setModeAndProcess(tracks[i]);
}
// Wait until text tracks are added
tracks.addEventListener('addtrack', function (event) {
setModeAndProcess(event.track);
});
};
/*
* Sets the track mode to one of 'disabled', 'hidden' or 'showing'
* @see https://github.com/videojs/video.js/blob/master/docs/guides/text-tracks.md
* Default behavior is to do nothing, @override if this is not desired
* @param track The text track to set the mode on
*/
cueTextTracks.setMetadataTrackMode = function (track) {
return;
};
/*
* Determines whether cue is an ad cue and returns the cue data.
* @param player A reference to the player
* @param cue The full cue object
* Returns the given cue by default @override if futher processing is required
* @return {Object} a useable ad cue or null if not supported
**/
cueTextTracks.getSupportedAdCue = function (player, cue) {
return cue;
};
/*
* Defines whether a cue is supported or not, potentially
* based on the player settings
* @param player A reference to the player
* @param cue The cue to be checked
* Default behavior is to return true, @override if this is not desired
* @return {Boolean}
*/
cueTextTracks.isSupportedAdCue = function (player, cue) {
return true;
};
/*
* Gets the id associated with a cue.
* @param cue The cue to extract an ID from
* @returns The first occurance of 'id' in the object,
* @override if this is not the desired cue id
**/
cueTextTracks.getCueId = function (player, cue) {
return cue.id;
};
/*
* Checks whether a cue has already been used
* @param cueId The Id associated with a cue
**/
var cueAlreadySeen = function cueAlreadySeen(player, cueId) {
return cueId !== undefined && player.ads.includedCues[cueId];
};
/*
* Indicates that a cue has been used
* @param cueId The Id associated with a cue
**/
var setCueAlreadySeen = function setCueAlreadySeen(player, cueId) {
if (cueId !== undefined && cueId !== '') {
player.ads.includedCues[cueId] = true;
}
};
/*
* This feature allows ad metadata tracks to be manipulated in ad implementations
* @param player A reference to the player
* @param cues The set of cues to work with
* @param processCue A method that uses a cue to make some
* ad request in the ad implementation
* @param [cancelAdsHandler] A method that dynamically cancels ads in the ad implementation
**/
cueTextTracks.processAdTrack = function (player, cues, processCue, cancelAdsHandler) {
player.ads.includedCues = {};
// loop over set of cues
for (var i = 0; i < cues.length; i++) {
var cue = cues[i];
var cueData = this.getSupportedAdCue(player, cue);
// Exit if this is not a supported cue
if (!this.isSupportedAdCue(player, cue)) {
videojs.log.warn('Skipping as this is not a supported ad cue.', cue);
return;
}
// Continue processing supported cue
var cueId = this.getCueId(player, cue);
var startTime = cue.startTime;
// Skip ad if cue was already used
if (cueAlreadySeen(player, cueId)) {
videojs.log('Skipping ad already seen with ID ' + cueId);
return;
}
// Optional dynamic ad cancellation
if (cancelAdsHandler) {
cancelAdsHandler(player, cueData, cueId, startTime);
}
// Process cue as an ad cue
processCue(player, cueData, cueId, startTime);
// Indicate that this cue has been used
setCueAlreadySeen(player, cueId);
}
};
function initCancelContentPlay(player, debug) {
if (debug) {
videojs.log('Using cancelContentPlay to block content playback');
}
// Listen to play events to "cancel" them afterward
player.on('play', cancelContentPlay);
}
/*
This feature makes sure the player is paused during ad loading.
It does this by pausing the player immediately after a "play" where ads will be requested,
then signalling that we should play after the ad is done.
*/
function cancelContentPlay() {
// this function is in the player's context
if (this.ads._shouldBlockPlay === false) {
// Only block play if the ad plugin is in a state when content
// playback should be blocked. This currently means during
// BeforePrerollState and PrerollState.
return;
}
// pause playback so ads can be handled.
if (!this.paused()) {
this.ads.debug('Playback was canceled by cancelContentPlay');
this.pause();
}
// When the 'content-playback' state is entered, this will let us know to play.
// This is needed if there is no preroll or if it errors, times out, etc.
this.ads._cancelledPlay = true;
}
var obj = {};
// This reference allows videojs to be mocked in unit tests
// while still using the available videojs import in the source code
// @see obj.testHook
var videojsReference = videojs;
/**
* Checks if middleware mediators are available and
* can be used on this platform.
* Currently we can only use mediators on desktop platforms.
*/
obj.isMiddlewareMediatorSupported = function () {
if (videojsReference.browser.IS_IOS || videojsReference.browser.IS_ANDROID) {
return false;
} else if (
// added when middleware was introduced in video.js
videojsReference.use &&
// added when mediators were introduced in video.js
videojsReference.middleware && videojsReference.middleware.TERMINATOR) {
return true;
}
return false;
};
obj.playMiddleware = function (player) {
return {
setSource: function setSource(srcObj, next) {
next(null, srcObj);
},
callPlay: function callPlay() {
// Block play calls while waiting for an ad, only if this is an
// ad supported player
if (player.ads && player.ads._shouldBlockPlay === true) {
player.ads.debug('Using playMiddleware to block content playback');
player.ads._playBlocked = true;
return videojsReference.middleware.TERMINATOR;
}
},
play: function play(terminated, playPromise) {
if (player.ads && player.ads._playBlocked && terminated) {
player.ads.debug('Play call to Tech was terminated.');
// Trigger play event to match the user's intent to play.
// The call to play on the Tech has been blocked, so triggering
// the event on the Player will not affect the Tech's playback state.
player.trigger('play');
// At this point the player has technically started
player.addClass('vjs-has-started');
// Reset playBlocked
player.ads._playBlocked = false;
// Safari issues a pause event when autoplay is blocked but other browsers
// do not, so we send a pause for consistency in those cases. This keeps the
// play button in the correct state if play is rejected.
} else if (playPromise && playPromise.catch) {
playPromise.catch(function (e) {
if (e.name === 'NotAllowedError' && !videojs.browser.IS_SAFARI) {
player.trigger('pause');
}
});
}
}
};
};
obj.testHook = function (testVjs) {
videojsReference = testVjs;
};
var playMiddleware = obj.playMiddleware,
isMiddlewareMediatorSupported = obj.isMiddlewareMediatorSupported;
/**
* Whether or not this copy of Video.js has the ads plugin.
*
* @return {boolean}
* If `true`, has the plugin. `false` otherwise.
*/
var hasAdsPlugin = function hasAdsPlugin() {
// Video.js 6 and 7 have a getPlugin method.
if (videojs.getPlugin) {
return Boolean(videojs.getPlugin('ads'));
}
// Video.js 5 does not have a getPlugin method, so check the player prototype.
var Player = videojs.getComponent('Player');
return Boolean(Player && Player.prototype.ads);
};
/**
* Register contrib-ads with Video.js, but provide protection for duplicate
* copies of the plugin. This could happen if, for example, a stitched ads
* plugin and a client-side ads plugin are included simultaneously with their
* own copies of contrib-ads.
*
* If contrib-ads detects a pre-existing duplicate, it will not register
* itself.
*
* Ad plugins using contrib-ads and anticipating that this could come into
* effect should verify that the contrib-ads they are using is of a compatible
* version.
*
* @param {Function} contribAdsPlugin
* The plugin function.
*
* @return {boolean}
* When `true`, the plugin was registered. When `false`, the plugin
* was not registered.
*/
function register(contribAdsPlugin) {
// If the ads plugin already exists, do not overwrite it.
if (hasAdsPlugin(videojs)) {
return false;
}
// Cross-compatibility with Video.js 6/7 and 5.
var registerPlugin = videojs.registerPlugin || videojs.plugin;
// Register this plugin with Video.js.
registerPlugin('ads', contribAdsPlugin);
// Register the play middleware with Video.js on script execution,
// to avoid a new playMiddleware factory being added for each player.
// The `usingContribAdsMiddleware_` flag is used to ensure that we only ever
// register the middleware once - despite the ability to de-register and
// re-register the plugin itself.
if (isMiddlewareMediatorSupported() && !videojs.usingContribAdsMiddleware_) {
// Register the play middleware
videojs.use('*', playMiddleware);
videojs.usingContribAdsMiddleware_ = true;
videojs.log.debug('Play middleware has been registered with videojs');
}
return true;
}
var States = /*#__PURE__*/function () {
function States() {}
States.getState = function getState(name) {
if (!name) {
return;
}
if (States.states_ && States.states_[name]) {
return States.states_[name];
}
};
States.registerState = function registerState(name, StateToRegister) {
if (typeof name !== 'string' || !name) {
throw new Error("Illegal state name, \"" + name + "\"; must be a non-empty string.");
}
if (!States.states_) {
States.states_ = {};
}
States.states_[name] = StateToRegister;
return StateToRegister;
};
return States;
}();
var State = /*#__PURE__*/function () {
State._getName = function _getName() {
return 'Anonymous State';
};
function State(player) {
this.player = player;
}
/*
* This is the only allowed way to perform state transitions. State transitions usually
* happen in player event handlers. They can also happen recursively in `init`. They
* should _not_ happen in `cleanup`.
*/
var _proto = State.prototype;
_proto.transitionTo = function transitionTo(NewState) {
var player = this.player;
// Since State is an abstract class, this will refer to
// the state that is extending this class
this.cleanup(player);
var newState = new NewState(player);
player.ads._state = newState;
player.ads.debug(this.constructor._getName() + ' -> ' + newState.constructor._getName());
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
newState.init.apply(newState, [player].concat(args));
}
/*
* Implemented by subclasses to provide initialization logic when transitioning
* to a new state.
*/;
_proto.init = function init() {}
/*
* Implemented by subclasses to provide cleanup logic when transitioning
* to a new state.
*/;
_proto.cleanup = function cleanup() {}
/*
* Default event handlers. Different states can override these to provide behaviors.
*/;
_proto.onPlay = function onPlay() {};
_proto.onPlaying = function onPlaying() {};
_proto.onEnded = function onEnded() {};
_proto.onAdEnded = function onAdEnded() {};
_proto.onAdsReady = function onAdsReady() {
videojs.log.warn('Unexpected adsready event');
};
_proto.onAdsError = function onAdsError() {};
_proto.onAdsCanceled = function onAdsCanceled() {};
_proto.onAdTimeout = function onAdTimeout() {};
_proto.onAdStarted = function onAdStarted() {};
_proto.onAdSkipped = function onAdSkipped() {};
_proto.onContentChanged = function onContentChanged() {};
_proto.onContentResumed = function onContentResumed() {};
_proto.onReadyForPostroll = function onReadyForPostroll() {
videojs.log.warn('Unexpected readyforpostroll event');
};
_proto.onNoPreroll = function onNoPreroll() {};
_proto.onNoPostroll = function onNoPostroll() {}
/*
* Method handlers. Different states can override these to provide behaviors.
*/;
_proto.startLinearAdMode = function startLinearAdMode() {
videojs.log.warn('Unexpected startLinearAdMode invocation ' + '(State via ' + this.constructor._getName() + ')');
};
_proto.endLinearAdMode = function endLinearAdMode() {
videojs.log.warn('Unexpected endLinearAdMode invocation ' + '(State via ' + this.constructor._getName() + ')');
};
_proto.skipLinearAdMode = function skipLinearAdMode() {
videojs.log.warn('Unexpected skipLinearAdMode invocation ' + '(State via ' + this.constructor._getName() + ')');
}
/*
* Overridden by ContentState and AdState. Should not be overriden elsewhere.
*/;
_proto.isAdState = function isAdState() {
throw new Error('isAdState unimplemented for ' + this.constructor._getName());
}
/*
* Overridden by Preroll and Postroll. Midrolls jump right into the ad break
* so there is no "waiting" state for them.
*/;
_proto.isWaitingForAdBreak = function isWaitingForAdBreak() {
return false;
}
/*
* Overridden by Preroll, Midroll, and Postroll.
*/;
_proto.isContentResuming = function isContentResuming() {
return false;
};
_proto.inAdBreak = function inAdBreak() {
return false;
}
/*
* Invoke event handler methods when events come in.
*/;
_proto.handleEvent = function handleEvent(type) {
var player = this.player;
if (type === 'play') {
this.onPlay(player);
} else if (type === 'adsready') {
this.onAdsReady(player);
} else if (type === 'adserror') {
this.onAdsError(player);
} else if (type === 'adscanceled') {
this.onAdsCanceled(player);
} else if (type === 'adtimeout') {
this.onAdTimeout(player);
} else if (type === 'ads-ad-started') {
this.onAdStarted(player);
} else if (type === 'ads-ad-skipped') {
this.onAdSkipped(player);
} else if (type === 'contentchanged') {
this.onContentChanged(player);
} else if (type === 'contentresumed') {
this.onContentResumed(player);
} else if (type === 'readyforpostroll') {
this.onReadyForPostroll(player);
} else if (type === 'playing') {
this.onPlaying(player);
} else if (type === 'ended') {
this.onEnded(player);
} else if (type === 'nopreroll') {
this.onNoPreroll(player);
} else if (type === 'nopostroll') {
this.onNoPostroll(player);
} else if (type === 'adended') {
this.onAdEnded(player);
}
};
return State;
}();
States.registerState('State', State);
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
/*
* This class contains logic for all ads, be they prerolls, midrolls, or postrolls.
* Primarily, this involves handling startLinearAdMode and endLinearAdMode.
* It also handles content resuming.
*/
var AdState = /*#__PURE__*/function (_State) {
_inheritsLoose(AdState, _State);
function AdState(player) {
var _this;
_this = _State.call(this, player) || this;
_this.contentResuming = false;
_this.waitingForAdBreak = false;
return _this;
}
/*
* Overrides State.isAdState
*/
var _proto = AdState.prototype;
_proto.isAdState = function isAdState() {
return true;
}
/*
* We end the content-resuming process on the playing event because this is the exact
* moment that content playback is no longer blocked by ads.
*/;
_proto.onPlaying = function onPlaying() {
var ContentPlayback = States.getState('ContentPlayback');
if (this.contentResuming) {
this.transitionTo(ContentPlayback);
}
}
/*
* If the ad plugin does not result in a playing event when resuming content after an
* ad, they should instead trigger a contentresumed event to signal that content should
* resume. The main use case for this is when ads are stitched into the content video.
*/;
_proto.onContentResumed = function onContentResumed() {
var ContentPlayback = States.getState('ContentPlayback');
if (this.contentResuming) {
this.transitionTo(ContentPlayback);
}
}
/*
* Check if we are in an ad state waiting for the ad plugin to start
* an ad break.
*/;
_proto.isWaitingForAdBreak = function isWaitingForAdBreak() {
return this.waitingForAdBreak;
}
/*
* Allows you to check if content is currently resuming after an ad break.
*/;
_proto.isContentResuming = function isContentResuming() {
return this.contentResuming;
}
/*
* Allows you to check if an ad break is in progress.
*/;
_proto.inAdBreak = function inAdBreak() {
return this.player.ads._inLinearAdMode === true;
};
return AdState;
}(State);
States.registerState('AdState', AdState);
var ContentState = /*#__PURE__*/function (_State) {
_inheritsLoose(ContentState, _State);
function ContentState() {
return _State.apply(this, arguments) || this;
}
var _proto = ContentState.prototype;
/*
* Overrides State.isAdState
*/
_proto.isAdState = function isAdState() {
return false;
}
/*
* Source change sends you back to preroll checks. contentchanged does not
* fire during ad breaks, so we don't need to worry about that.
*/;
_proto.onContentChanged = function onContentChanged(player) {
var BeforePreroll = States.getState('BeforePreroll');
var Preroll = States.getState('Preroll');
player.ads.debug('Received contentchanged event (ContentState)');
if (player.paused()) {
this.transitionTo(BeforePreroll);
} else {
this.transitionTo(Preroll, false);
player.pause();
player.ads._pausedOnContentupdate = true;
}
};
return ContentState;
}(State);
States.registerState('ContentState', ContentState);
var ContentState$1 = States.getState('ContentState');
var AdsDone = /*#__PURE__*/function (_ContentState) {
_inheritsLoose(AdsDone, _ContentState);
function AdsDone() {
return _ContentState.apply(this, arguments) || this;
}
/*
* Allows state name to be logged even after minification.
*/
AdsDone._getName = function _getName() {
return 'AdsDone';
}
/*
* For state transitions to work correctly, initialization should
* happen here, not in a constructor.
*/;
var _proto = AdsDone.prototype;
_proto.init = function init(player) {
// From now on, `ended` events won't be redispatched
player.ads._contentHasEnded = true;
player.trigger('ended');
}
/*
* Midrolls do not play after ads are done.
*/;
_proto.startLinearAdMode = function startLinearAdMode() {
videojs.log.warn('Unexpected startLinearAdMode invocation (AdsDone)');
};
return AdsDone;
}(ContentState$1);
States.registerState('AdsDone', AdsDone);
/*
The snapshot feature is responsible for saving the player state before an ad, then
restoring the player state after an ad.
*/
var tryToResumeTimeout_;
/*
* Returns an object that captures the portions of player state relevant to
* video playback. The result of this function can be passed to
* restorePlayerSnapshot with a player to return the player to the state it
* was in when this function was invoked.
* @param {Object} player The videojs player object
*/
function getPlayerSnapshot(player) {
var currentTime;
if (videojs.browser.IS_IOS && player.ads.isLive(player)) {
// Record how far behind live we are
if (player.seekable().length > 0) {
currentTime = player.currentTime() - player.seekable().end(0);
} else {
currentTime = player.currentTime();
}
} else {
currentTime = player.currentTime();
}
var tech = player.$('.vjs-tech');
var tracks = player.textTracks ? player.textTracks() : [];
var suppressedTracks = [];
var snapshotObject = {
ended: player.ended(),
currentSrc: player.currentSrc(),
sources: player.currentSources(),
src: player.tech_.src(),
currentTime: currentTime,
type: player.currentType()
};
if (tech) {
snapshotObject.style = tech.getAttribute('style');
}
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
suppressedTracks.push({
track: track,
mode: track.mode
});
track.mode = 'disabled';
}
snapshotObject.suppressedTracks = suppressedTracks;
return snapshotObject;
}
/*
* Attempts to modify the specified player so that its state is equivalent to
* the state of the snapshot.
* @param {Object} player - the videojs player object
* @param {Object} snapshotObject - the player state to apply
*/
function restorePlayerSnapshot(player, callback) {
var snapshotObject = player.ads.snapshot;
if (callback === undefined) {
callback = function callback() {};
}
if (player.ads.disableNextSnapshotRestore === true) {
player.ads.disableNextSnapshotRestore = false;
delete player.ads.snapshot;
callback();
return;
}
// The playback tech
var tech = player.$('.vjs-tech');
// the number of[ remaining attempts to restore the snapshot
var attempts = 20;
var suppressedTracks = snapshotObject.suppressedTracks;
var trackSnapshot;
var restoreTracks = function restoreTracks() {
for (var i = 0; i < suppressedTracks.length; i++) {
trackSnapshot = suppressedTracks[i]