videojs-contrib-ads
Version:
A framework that provides common functionality needed by video advertisement libraries working with video.js.
1,756 lines (1,370 loc) • 67.5 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var videojs = _interopDefault(require('video.js'));
var window = _interopDefault(require('global/window'));
var document = _interopDefault(require('global/document'));
/*
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
// * A single adplaying event when an ad begins
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);
}
// adplaying was already sent due to cancelContentPlay. Avoid sending another.
} else if (player.ads._cancelledPlay) {
cancelEvent(player, 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()) {
// The true ended event fired either after the postroll
// or because there was no postroll.
if (player.ads.isContentResuming()) {
return;
}
// Prefix ended due to ad ending.
prefixEvent(player, 'ad', event);
// Prefix ended due to content ending before preroll check
} else if (!player.ads._contentHasEnded) {
prefixEvent(player, 'content', event);
}
};
// 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 will always cause the content to play. Therefor, contrib-ads
// must always cancelContentPlay if there is any possible chance the play caused the
// content to play, 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. A better solution
// would be to have a way to intercept play events rather than "cancel" them by pausing
// after each one. To be continued...
var handlePlay = function handlePlay(player, event) {
var resumingAfterNoPreroll = player.ads._cancelledPlay && !player.ads.isInAdMode();
if (player.ads.inAdBreak()) {
prefixEvent(player, 'ad', event);
} else if (player.ads.isContentResuming() || resumingAfterNoPreroll) {
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);
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var asyncGenerator = function () {
function AwaitValue(value) {
this.value = value;
}
function AsyncGenerator(gen) {
var front, back;
function send(key, arg) {
return new Promise(function (resolve, reject) {
var request = {
key: key,
arg: arg,
resolve: resolve,
reject: reject,
next: null
};
if (back) {
back = back.next = request;
} else {
front = back = request;
resume(key, arg);
}
});
}
function resume(key, arg) {
try {
var result = gen[key](arg);
var value = result.value;
if (value instanceof AwaitValue) {
Promise.resolve(value.value).then(function (arg) {
resume("next", arg);
}, function (arg) {
resume("throw", arg);
});
} else {
settle(result.done ? "return" : "normal", result.value);
}
} catch (err) {
settle("throw", err);
}
}
function settle(type, value) {
switch (type) {
case "return":
front.resolve({
value: value,
done: true
});
break;
case "throw":
front.reject(value);
break;
default:
front.resolve({
value: value,
done: false
});
break;
}
front = front.next;
if (front) {
resume(front.key, front.arg);
} else {
back = null;
}
}
this._invoke = send;
if (typeof gen.return !== "function") {
this.return = undefined;
}
}
if (typeof Symbol === "function" && Symbol.asyncIterator) {
AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
return this;
};
}
AsyncGenerator.prototype.next = function (arg) {
return this._invoke("next", arg);
};
AsyncGenerator.prototype.throw = function (arg) {
return this._invoke("throw", arg);
};
AsyncGenerator.prototype.return = function (arg) {
return this._invoke("return", arg);
};
return {
wrap: function (fn) {
return function () {
return new AsyncGenerator(fn.apply(this, arguments));
};
},
await: function (value) {
return new AwaitValue(value);
}
};
}();
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
var possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
/*
This feature provides an optional method for ad integrations 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 integrations 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) {
if (uriEncode === undefined) {
uriEncode = false;
}
var macros = {};
if (customMacros !== undefined) {
macros = customMacros;
}
// Static macros
macros['{player.id}'] = this.options_['data-player'];
macros['{mediainfo.id}'] = this.mediainfo ? this.mediainfo.id : '';
macros['{mediainfo.name}'] = this.mediainfo ? this.mediainfo.name : '';
macros['{mediainfo.description}'] = this.mediainfo ? this.mediainfo.description : '';
macros['{mediainfo.tags}'] = this.mediainfo ? this.mediainfo.tags : '';
macros['{mediainfo.reference_id}'] = this.mediainfo ? this.mediainfo.reference_id : '';
macros['{mediainfo.duration}'] = this.mediainfo ? this.mediainfo.duration : '';
macros['{mediainfo.ad_keys}'] = this.mediainfo ? this.mediainfo.ad_keys : '';
macros['{player.duration}'] = this.duration();
macros['{timestamp}'] = new Date().getTime();
macros['{document.referrer}'] = document.referrer;
macros['{window.location.href}'] = window.location.href;
macros['{random}'] = Math.floor(Math.random() * 1000000000000);
// Custom fields in mediainfo
customFields(this.mediainfo, macros, 'custom_fields');
customFields(this.mediainfo, macros, 'customFields');
// 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 = void 0;
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]];
}
}
var type = typeof value === 'undefined' ? 'undefined' : _typeof(value);
// Only allow certain types of values. Anything else is probably a mistake.
if (value === null) {
return 'null';
} else if (value === undefined) {
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);
});
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);
}
};
var State = function () {
function State(player) {
classCallCheck(this, State);
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`.
*/
State.prototype.transitionTo = function transitionTo(NewState) {
var player = this.player;
var previousState = this;
previousState.cleanup();
var newState = new NewState(player);
player.ads._state = newState;
player.ads.debug(previousState.constructor.name + ' -> ' + newState.constructor.name);
for (var _len = arguments.length, args = 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.
*/
State.prototype.init = function init() {};
/*
* Implemented by subclasses to provide cleanup logic when transitioning
* to a new state.
*/
State.prototype.cleanup = function cleanup() {};
/*
* Default event handlers. Different states can override these to provide behaviors.
*/
State.prototype.onPlay = function onPlay() {};
State.prototype.onPlaying = function onPlaying() {};
State.prototype.onEnded = function onEnded() {};
State.prototype.onAdsReady = function onAdsReady() {
videojs.log.warn('Unexpected adsready event');
};
State.prototype.onAdsError = function onAdsError() {};
State.prototype.onAdsCanceled = function onAdsCanceled() {};
State.prototype.onAdTimeout = function onAdTimeout() {};
State.prototype.onAdStarted = function onAdStarted() {};
State.prototype.onContentChanged = function onContentChanged() {};
State.prototype.onContentResumed = function onContentResumed() {};
State.prototype.onContentEnded = function onContentEnded() {
videojs.log.warn('Unexpected contentended event');
};
State.prototype.onNoPreroll = function onNoPreroll() {};
State.prototype.onNoPostroll = function onNoPostroll() {};
/*
* Method handlers. Different states can override these to provide behaviors.
*/
State.prototype.startLinearAdMode = function startLinearAdMode() {
videojs.log.warn('Unexpected startLinearAdMode invocation ' + '(State via ' + this.constructor.name + ')');
};
State.prototype.endLinearAdMode = function endLinearAdMode() {
videojs.log.warn('Unexpected endLinearAdMode invocation ' + '(State via ' + this.constructor.name + ')');
};
State.prototype.skipLinearAdMode = function skipLinearAdMode() {
videojs.log.warn('Unexpected skipLinearAdMode invocation ' + '(State via ' + this.constructor.name + ')');
};
/*
* Overridden by ContentState and AdState. Should not be overriden elsewhere.
*/
State.prototype.isAdState = function isAdState() {
throw new Error('isAdState unimplemented for ' + this.constructor.name);
};
/*
* Overridden by PrerollState, MidrollState, and PostrollState.
*/
State.prototype.isContentResuming = function isContentResuming() {
return false;
};
State.prototype.inAdBreak = function inAdBreak() {
return false;
};
/*
* Invoke event handler methods when events come in.
*/
State.prototype.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 === 'contentchanged') {
this.onContentChanged(player);
} else if (type === 'contentresumed') {
this.onContentResumed(player);
} else if (type === 'contentended') {
this.onContentEnded(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);
}
};
return State;
}();
/*
* 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 = function (_State) {
inherits(AdState, _State);
function AdState(player) {
classCallCheck(this, AdState);
var _this = possibleConstructorReturn(this, _State.call(this, player));
_this.contentResuming = false;
return _this;
}
/*
* Overrides State.isAdState
*/
AdState.prototype.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.
*/
AdState.prototype.onPlaying = function onPlaying() {
if (this.contentResuming) {
this.transitionTo(ContentPlayback);
}
};
/*
* If the integration 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.
*/
AdState.prototype.onContentResumed = function onContentResumed() {
if (this.contentResuming) {
this.transitionTo(ContentPlayback);
}
};
/*
* Allows you to check if content is currently resuming after an ad break.
*/
AdState.prototype.isContentResuming = function isContentResuming() {
return this.contentResuming;
};
/*
* Allows you to check if an ad break is in progress.
*/
AdState.prototype.inAdBreak = function inAdBreak() {
return this.player.ads._inLinearAdMode === true;
};
return AdState;
}(State);
var ContentState = function (_State) {
inherits(ContentState, _State);
function ContentState() {
classCallCheck(this, ContentState);
return possibleConstructorReturn(this, _State.apply(this, arguments));
}
/*
* Overrides State.isAdState
*/
ContentState.prototype.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.
*/
ContentState.prototype.onContentChanged = function onContentChanged(player) {
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);
/*
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(player) {
if (player.ads.cancelPlayTimeout) {
// another cancellation is already in flight, so do nothing
return;
}
// The timeout is necessary because pausing a video element while processing a `play`
// event on iOS can cause the video element to continuously toggle between playing and
// paused states.
player.ads.cancelPlayTimeout = player.setTimeout(function () {
// deregister the cancel timeout so subsequent cancels are scheduled
player.ads.cancelPlayTimeout = null;
if (!player.ads.isInAdMode()) {
return;
}
// pause playback so ads can be handled.
if (!player.paused()) {
player.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.
player.ads._cancelledPlay = true;
}, 1);
}
/*
The snapshot feature is responsible for saving the player state before an ad, then
restoring the player state after an ad.
*/
/*
* 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 = void 0;
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(),
src: player.tech_.src(),
currentTime: currentTime,
type: player.currentType()
};
if (tech) {
snapshotObject.nativePoster = tech.poster;
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, snapshotObject) {
if (player.ads.disableNextSnapshotRestore === true) {
player.ads.disableNextSnapshotRestore = false;
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 = void 0;
var restoreTracks = function restoreTracks() {
for (var i = 0; i < suppressedTracks.length; i++) {
trackSnapshot = suppressedTracks[i];
trackSnapshot.track.mode = trackSnapshot.mode;
}
};
// finish restoring the playback state
var resume = function resume() {
var currentTime = void 0;
if (videojs.browser.IS_IOS && player.ads.isLive(player)) {
if (snapshotObject.currentTime < 0) {
// Playback was behind real time, so seek backwards to match
if (player.seekable().length > 0) {
currentTime = player.seekable().end(0) + snapshotObject.currentTime;
} else {
currentTime = player.currentTime();
}
player.currentTime(currentTime);
}
} else if (snapshotObject.ended) {
player.currentTime(player.duration());
} else {
player.currentTime(snapshotObject.currentTime);
}
// Resume playback if this wasn't a postroll
if (!snapshotObject.ended) {
player.play();
}
// if we added autoplay to force content loading on iOS, remove it now
// that it has served its purpose
if (player.ads.shouldRemoveAutoplay_) {
player.autoplay(false);
player.ads.shouldRemoveAutoplay_ = false;
}
};
// determine if the video element has loaded enough of the snapshot source
// to be ready to apply the rest of the state
var tryToResume = function tryToResume() {
// tryToResume can either have been called through the `contentcanplay`
// event or fired through setTimeout.
// When tryToResume is called, we should make sure to clear out the other
// way it could've been called by removing the listener and clearing out
// the timeout.
player.off('contentcanplay', tryToResume);
if (player.ads.tryToResumeTimeout_) {
player.clearTimeout(player.ads.tryToResumeTimeout_);
player.ads.tryToResumeTimeout_ = null;
}
// Tech may have changed depending on the differences in sources of the
// original video and that of the ad
tech = player.el().querySelector('.vjs-tech');
if (tech.readyState > 1) {
// some browsers and media aren't "seekable".
// readyState greater than 1 allows for seeking without exceptions
return resume();
}
if (tech.seekable === undefined) {
// if the tech doesn't expose the seekable time ranges, try to
// resume playback immediately
return resume();
}
if (tech.seekable.length > 0) {
// if some period of the video is seekable, resume playback
return resume();
}
// delay a bit and then check again unless we're out of attempts
if (attempts--) {
player.setTimeout(tryToResume, 50);
} else {
try {
resume();
} catch (e) {
videojs.log.warn('Failed to resume the content after an advertisement', e);
}
}
};
if (snapshotObject.nativePoster) {
tech.poster = snapshotObject.nativePoster;
}
if ('style' in snapshotObject) {
// overwrite all css style properties to restore state precisely
tech.setAttribute('style', snapshotObject.style || '');
}
// Determine whether the player needs to be restored to its state
// before ad playback began. With a custom ad display or burned-in
// ads, the content player state hasn't been modified and so no
// restoration is required
if (player.ads.videoElementRecycled()) {
// on ios7, fiddling with textTracks too early will cause safari to crash
player.one('contentloadedmetadata', restoreTracks);
// adding autoplay guarantees that Safari will load the content so we can
// seek back to the correct time after ads
if (videojs.browser.IS_IOS && !player.autoplay()) {
player.autoplay(true);
// if we get here, the player was not originally configured to autoplay,
// so we should remove it after it has served its purpose
player.ads.shouldRemoveAutoplay_ = true;
}
// if the src changed for ad playback, reset it
player.src({ src: snapshotObject.currentSrc, type: snapshotObject.type });
// and then resume from the snapshots time once the original src has loaded
// in some browsers (firefox) `canplay` may not fire correctly.
// Reace the `canplay` event with a timeout.
player.one('contentcanplay', tryToResume);
player.ads.tryToResumeTimeout_ = player.setTimeout(tryToResume, 2000);
} else {
// if we didn't change the src, just restore the tracks
restoreTracks();
// we don't need to check snapshotObject.ended here because the content video
// element wasn't recycled
if (!player.ended()) {
// the src didn't change and this wasn't a postroll
// just resume playback at the current time.
player.play();
}
}
}
/*
* Encapsulates logic for starting and ending ad breaks. An ad break
* is the time between startLinearAdMode and endLinearAdMode. The ad
* integration may play 0 or more ads during this time.
*/
function start(player) {
player.ads.debug('Starting ad break');
player.ads._inLinearAdMode = true;
// No longer does anything, used to move us to ad-playback
player.trigger('adstart');
// Capture current player state snapshot
if (!player.ads.shouldPlayContentBehindAd(player)) {
player.ads.snapshot = getPlayerSnapshot(player);
}
// Mute the player behind the ad
if (player.ads.shouldPlayContentBehindAd(player)) {
player.ads.preAdVolume_ = player.volume();
player.volume(0);
}
// Add css to the element to indicate and ad is playing.
player.addClass('vjs-ad-playing');
// We should remove the vjs-live class if it has been added in order to
// show the adprogress control bar on Android devices for falsely
// determined LIVE videos due to the duration incorrectly reported as Infinity
if (player.hasClass('vjs-live')) {
player.removeClass('vjs-live');
}
// This removes the native poster so the ads don't show the content
// poster if content element is reused for ad playback. The snapshot
// will restore it afterwards.
player.ads.removeNativePoster();
}
function end(player) {
player.ads.debug('Ending ad break');
player.ads.adType = null;
player.ads._inLinearAdMode = false;
// Signals the end of the ad break to anyone listening.
player.trigger('adend');
player.removeClass('vjs-ad-playing');
// We should add the vjs-live class back if the video is a LIVE video
// If we dont do this, then for a LIVE Video, we will get an incorrect
// styled control, which displays the time for the video
if (player.ads.isLive(player)) {
player.addClass('vjs-live');
}
if (!player.ads.shouldPlayContentBehindAd(player)) {
restorePlayerSnapshot(player, player.ads.snapshot);
}
// Reset the volume to pre-ad levels
if (player.ads.shouldPlayContentBehindAd(player)) {
player.volume(player.ads.preAdVolume_);
}
}
var obj = { start: start, end: end };
/*
* This state encapsulates waiting for prerolls, preroll playback, and
* content restoration after a preroll.
*/
var Preroll = function (_AdState) {
inherits(Preroll, _AdState);
function Preroll() {
classCallCheck(this, Preroll);
return possibleConstructorReturn(this, _AdState.apply(this, arguments));
}
Preroll.prototype.init = function init(player, adsReady) {
// Loading spinner from now until ad start or end of ad break.
player.addClass('vjs-ad-loading');
// Determine preroll timeout based on plugin settings
var timeout = player.ads.settings.timeout;
if (typeof player.ads.settings.prerollTimeout === 'number') {
timeout = player.ads.settings.prerollTimeout;
}
// Start the clock ticking for ad timeout
this._timeout = player.setTimeout(function () {
player.trigger('adtimeout');
}, timeout);
// If adsready already happened, lets get started. Otherwise,
// wait until onAdsReady.
if (adsReady) {
this.handleAdsReady();
} else {
this.adsReady = false;
}
};
Preroll.prototype.onAdsReady = function onAdsReady(player) {
if (!player.ads.inAdBreak() && !player.ads.isContentResuming()) {
player.ads.debug('Received adsready event (Preroll)');
this.handleAdsReady();
} else {
videojs.log.warn('Unexpected adsready event (Preroll)');
}
};
/*
* Ad integration is ready. Let's get started on this preroll.
*/
Preroll.prototype.handleAdsReady = function handleAdsReady() {
this.adsReady = true;
if (this.player.ads.nopreroll_) {
this.noPreroll();
} else {
this.readyForPreroll();
}
};
/*
* Helper to call a callback only after a loadstart event.
* If we start content or ads before loadstart, loadstart
* will not be prefixed correctly.
*/
Preroll.prototype.afterLoadStart = function afterLoadStart(callback) {
var player = this.player;
if (player.ads._hasThereBeenALoadStartDuringPlayerLife) {
callback();
} else {
player.ads.debug('Waiting for loadstart...');
player.one('loadstart', function () {
player.ads.debug('Received loadstart event');
callback();
});
}
};
/*
* If there is no preroll, play content instead.
*/
Preroll.prototype.noPreroll = function noPreroll() {
var _this2 = this;
this.afterLoadStart(function () {
_this2.player.ads.debug('Skipping prerolls due to nopreroll event (Preroll)');
_this2.transitionTo(ContentPlayback);
});
};
/*
* Fire the readyforpreroll event. If loadstart hasn't happened yet,
* wait until loadstart first.
*/
Preroll.prototype.readyForPreroll = function readyForPreroll() {
var player = this.player;
this.afterLoadStart(function () {
player.ads.debug('Triggered readyforpreroll event (Preroll)');
player.trigger('readyforpreroll');
});
};
/*
* Don't allow the content to start playing while we're dealing with ads.
*/
Preroll.prototype.onPlay = function onPlay(player) {
player.ads.debug('Received play event (Preroll)');
if (!this.inAdBreak() && !this.isContentResuming()) {
cancelContentPlay(this.player);
}
};
/*
* adscanceled cancels all ads for the source. Play content now.
*/
Preroll.prototype.onAdsCanceled = function onAdsCanceled(player) {
var _this3 = this;
player.ads.debug('adscanceled (Preroll)');
this.afterLoadStart(function () {
_this3.transitionTo(ContentPlayback);
});
};
/*
* An ad error occured. Play content instead.
*/
Preroll.prototype.onAdsError = function onAdsError(player) {
var _this4 = this;
videojs.log('adserror (Preroll)');
// In the future, we may not want to do this automatically.
// Integrations should be able to choose to continue the ad break
// if there was an error.
if (this.inAdBreak()) {
player.ads.endLinearAdMode();
}
this.afterLoadStart(function () {
_this4.transitionTo(ContentPlayback);
});
};
/*
* Integration invoked startLinearAdMode, the ad break starts now.
*/
Preroll.prototype.startLinearAdMode = function startLinearAdMode() {
var player = this.player;
if (this.adsReady && !player.ads.inAdBreak() && !this.isContentResuming()) {
player.clearTimeout(this._timeout);
player.ads.adType = 'preroll';
obj.start(player);
} else {
videojs.log.warn('Unexpected startLinearAdMode invocation (Preroll)');
}
};
/*
* An ad has actually started playing.
* Remove the loading spinner.
*/
Preroll.prototype.onAdStarted = function onAdStarted(player) {
player.removeClass('vjs-ad-loading');
};
/*
* Integration invoked endLinearAdMode, the ad break ends now.
*/
Preroll.prototype.endLinearAdMode = function endLinearAdMode() {
var player = this.player;
if (this.inAdBreak()) {
player.removeClass('vjs-ad-loading');
obj.end(player);
this.contentResuming = true;
}
};
/*
* Ad skipped by integration. Play content instead.
*/
Preroll.prototype.skipLinearAdMode = function skipLinearAdMode() {
var _this5 = this;
var player = this.player;
if (player.ads.inAdBreak() || this.isContentResuming()) {
videojs.log.warn('Unexpected skipLinearAdMode invocation');
} else {
this.afterLoadStart(function () {
player.trigger('adskip');
player.ads.debug('skipLinearAdMode (Preroll)');
_this5.transitionTo(ContentPlayback);
});
}
};
/*
* Prerolls took too long! Play content instead.
*/
Preroll.prototype.onAdTimeout = function onAdTimeout(player) {
var _this6 = this;
this.afterLoadStart(function () {
player.ads.debug('adtimeout (Preroll)');
_this6.transitionTo(ContentPlayback);
});
};
/*
* Check if nopreroll event was too late before handling it.
*/
Preroll.prototype.onNoPreroll = function onNoPreroll(player) {
if (player.ads.inAdBreak() || this.isContentResuming()) {
videojs.log.warn('Unexpected nopreroll event (Preroll)');
} else {
this.noPreroll();
}
};
/*
* Cleanup timeouts and spinner.
*/
Preroll.prototype.cleanup = function cleanup() {
var player = this.player;
if (!player.ads._hasThereBeenALoadStartDuringPlayerLife) {
videojs.log.warn('Leaving Preroll state before loadstart event can cause issues.');
}
player.removeClass('vjs-ad-loading');
player.clearTimeout(this._timeout);
};
return Preroll;
}(AdState);
var Midroll = function (_AdState) {
inherits(Midroll, _AdState);
function Midroll() {
classCallCheck(this, Midroll);
return possibleConstructorReturn(this, _AdState.apply(this, arguments));
}
/*
* Midroll breaks happen when the integration calls startLinearAdMode,
* which can happen at any time during content playback.
*/
Midroll.prototype.init = function init(player) {
player.ads.adType = 'midroll';
obj.start(player);
};
/*
* Midroll break is done.
*/
Midroll.prototype.endLinearAdMode = function endLinearAdMode() {
var player = this.player;
if (this.inAdBreak()) {
this.contentResuming = true;
obj.end(player);
}
};
/*
* End midroll break if there is an error.
*/
Midroll.prototype.onAdsError = function onAdsError(player) {
// In the future, we may not want to do this automatically.
// Integrations should be able to choose to continue the ad break
// if there was an error.
if (this.inAdBreak()) {
player.ads.endLinearAdMode();
}
};
return Midroll;
}(AdState);
var Postroll = function (_AdState) {
inherits(Postroll, _AdState);
function Postroll() {
classCallCheck(this, Postroll);
return possibleConstructorReturn(this, _AdState.apply(this, arguments));
}
Postroll.prototype.init = function init(player) {
var _this2 = this;
// Legacy name that now simply means "handling postrolls".
player.ads._contentEnding = true;
// Start postroll process.
if (!player.ads.nopostroll_) {
player.addClass('vjs-ad-loading');
// Determine postroll timeout based on plugin settings
var timeout = player.ads.settings.timeout;
if (typeof player.ads.settings.postrollTimeout === 'number') {
timeout = player.ads.settings.postrollTimeout;
}
this._postrollTimeout = player.setTimeout(function () {
player.trigger('adtimeout');
}, timeout);
// No postroll, ads are done
} else {
player.setTimeout(function () {
player.ads.debug('Triggered ended event (no postroll)');
_this2.contentResuming = true;
player.trigger('ended');
}, 1);
}
};
/*
* Start the postroll if it's not too late.
*/
Postroll.prototype.startLinearAdMode = function startLinearAdMode() {
var player = this.player;
if (!player.ads.inAdBreak() && !this.isContentResuming()) {
player.ads.adType = 'postroll';
player.clearTimeout(this._postrollTimeout);
obj.start(player);
} else {
videojs.log.warn('Unexpected startLinearAdMode invocation (Postroll)');
}
};
/*
* An ad has actually started playing.
* Remove the loading spinner.
*/
Postroll.prototype.onAdStarted = function onAdStarted(player) {
player.removeClass('vjs-ad-loading');
};
Postroll.prototype.endLinearAdMode = function endLinearAdMode() {
var player = this.player;
if (this.inAdBreak()) {
player.removeClass('vjs-ad-loading');
obj.end(player);
this.contentResuming = true;
player.ads.debug('Triggered ended event (endLinearAdMode)');
player.trigger('ended');
}
};
Postroll.prototype.skipLinearAdMode = function skipLinearAdMode() {
var player = this.player;
if (player.ads.inAdBreak() || this.isContentResuming()) {
videojs.log.warn('Unexpected skipLinearAdMode invocation');
} else {
player.ads.debug('Postroll abort (skipLinearAdMode)');
player.trigger('adskip');
this.abort();
}
};
Postroll.prototype.onAdTimeout = function onAdTimeout(player) {
player.ads.debug('Postroll abort (adtimeout)');
this.abort();
};
Postroll.prototype.onAdsError = function onAdsError(player) {
player.ads.debug('Postroll abort (adserror)');
// In the future, we may not want to do this automatically.
// Integrations should be able to choose to continue the ad break
// if there was an error.
if (player.ads.inAdBreak()) {
player.ads.endLinearAdMode();
}
this.abort();
};
Postroll.prototype.onEnded = function onEnded() {
if (this.isContentResuming()) {
this.transitionTo(AdsDone);
} else {
videojs.log.warn('Unexpected ended event during postroll');
}
};
Postroll.prototype.onContentChanged = function onContentChanged(player) {
if (this.isContentResuming()) {
this.transitionTo(BeforePreroll);
} else if (!this.inAdBreak()) {
this.transitionTo(Preroll);
}
};
Postroll.prototype.onNoPostroll = function onNoPostroll(player) {
if (!this.isContentResuming() && !this.inAdBreak()) {
this.transitionTo(AdsDone);
} else {
videojs.log.warn('Unexpected nopostroll event (Postroll)');
}
};
Postroll.prototype.abort = function abort() {
var player = this.player;
this.contentResuming = true;
player.removeClass('vjs-ad-loading');
player.ads.debug('Triggered ended event (postroll abort)');
player.trigger('ended');
};
Postroll.prototype.cleanup = function cleanup() {
var player = this.player;
player.clearTimeout(this._postrollTimeout);
player.ads._contentEnding = false;
};
return Postroll;
}(AdState);
/*
* This is the initial state for a player with an ad plugin. Normally, it remains in this
* state until a "play" event is seen. After that, we enter the Preroll state to check for
* prerolls. This happens regardless of whether or not any prerolls ultimately will play.
* Errors and other conditions may lead us directly from here to ContentPlayback.
*/
var BeforePreroll = function (_ContentState) {
inherits(BeforePreroll, _ContentState);
function BeforePreroll() {
classCallCheck(this, BeforePreroll);
return possibleConstructorReturn(this, _ContentState.apply(this, arguments));
}
BeforePreroll.prototype.init = function init(player) {
this.adsReady = false;
};
/*
* The integration may trigger adsready before the play request. If so,
* we record that adsready already happened so the Preroll state will know.
*/
BeforePreroll.prototype.onAdsReady = function onAdsReady(player) {
player.ads.debug('Received adsready event (BeforePreroll)');
this.adsReady = true;
};
/*
* Ad mode officially begins on the play request, because at this point
* content playback is blocked by the ad plugin.
*/
BeforePreroll.prototype.onPlay = function onPlay(player) {
player.ads.debug('Received play event (BeforePreroll)');
// Don't start content playback yet
cancelContentPlay(player);
// Check for prerolls
this.transitionTo(Preroll, this.adsReady);
};
/*
* All ads for the entire video are canceled.
*/
BeforePreroll.prototype.onAdsCanceled = function onAdsCanceled(player) {
player.ads.de