/*! @name videojs-contrib-ads @version 6.9.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 = "6.9.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. */ 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['{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.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]]; } } 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.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 === '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; subClass.__proto__ = superClass; } /* * 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]; trackSnapshot.track.mode = trackSnapshot.mode; } }; // Finish restoring the playback state. // This only happens if the content video element was reused for ad playback. var resume = function resume() { var currentTime; // Live video on iOS has special logic to try to seek to the right place after // an ad. 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); } // iOS live play after restore if player was paused (would not be paused if // ad played muted behind ad) if (player.paused()) { var playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch(function (error) { videojs.log.warn('Play promise rejected in IOS snapshot resume', error); }); } } // Restore the video position after an ad. // We check snapshotObject.ended because the content starts at the beginning again // after being restored. } else if (snapshotObject.ended) { // For postrolls, seek to the player's current duration. // It could be different from the snapshot's currentTime due to // inaccuracy in HLS. player.currentTime(player.duration()); } else { // Prerolls and midrolls, just seek to the player time before the ad. player.currentTime(snapshotObject.currentTime); var _playPromise = player.play(); if (_playPromise && _playPromise.catch) { _playPromise.catch(function (error) { videojs.log.warn('Play promise rejected in snapshot resume', error); }); } } // 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. // This only happens if the content video element was reused for ad playback. 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 (tryToResumeTimeout_) { player.clearTimeout(tryToResumeTimeout_); } // 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 ('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()) { // Snapshot restore is done, so now we're really finished. player.one('resumeended', function () { delete player.ads.snapshot; callback(); }); // 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(snapshotObject.sources); // 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); 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. var playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch(function (error) { videojs.log.warn('Play promise rejected in snapshot restore', error); }); } } // snapshot restore is complete delete player.ads.snapshot; callback(); } } /* * Encapsulates logic for starting and ending ad breaks. An ad break * is the time between startLinearAdMode and endLinearAdMode. The ad * plugin 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.shouldTakeSnapshots()) { 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. player.ads.removeNativePoster(); } function end(player, callback) { player.ads.debug('Ending ad break'); if (callback === undefined) { callback = function callback() {}; } 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'); } // Restore snapshot if (player.ads.shouldTakeSnapshots()) { restorePlayerSnapshot(player, callback); // Reset the volume to pre-ad levels } else { player.volume(player.ads.preAdVolume_); callback(); } } var obj$1 = { start: start, end: end }; var AdState$1 = States.getState('AdState'); /* * This state encapsulates waiting for prerolls, preroll playback, and * content restoration after a preroll. */ var Preroll = /*#__PURE__*/ function (_AdState) { _inheritsLoose(Preroll, _AdState); function Preroll() { return _AdState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ Preroll._getName = function _getName() { return 'Preroll'; } /* * For state transitions to work correctly, initialization should * happen here, not in a constructor. */ ; var _proto = Preroll.prototype; _proto.init = function init(player, adsReady, shouldResumeToContent) { this.waitingForAdBreak = true; // Loading spinner from now until ad start or end of ad break. player.addClass('vjs-ad-loading'); // If adserror, adscanceled, nopreroll or skipLinearAdMode already // ocurred, resume to content immediately if (shouldResumeToContent || player.ads.nopreroll_) { return this.resumeAfterNoPreroll(player); } // 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; } } /* * Adsready event after play event. */ ; _proto.onAdsReady = function onAdsReady(player) { if (!player.ads.inAdBreak()) { player.ads.debug('Received adsready event (Preroll)'); this.handleAdsReady(); } else { videojs.log.warn('Unexpected adsready event (Preroll)'); } } /* * Ad plugin is ready. Let's get started on this preroll. */ ; _proto.handleAdsReady = function handleAdsReady() { this.adsReady = true; 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. */ ; _proto.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. */ ; _proto.noPreroll = function noPreroll() { var _this = this; this.afterLoadStart(function () { _this.player.ads.debug('Skipping prerolls due to nopreroll event (Preroll)'); _this.resumeAfterNoPreroll(_this.player); }); } /* * Fire the readyforpreroll event. If loadstart hasn't happened yet, * wait until loadstart first. */ ; _proto.readyForPreroll = function readyForPreroll() { var player = this.player; this.afterLoadStart(function () { player.ads.debug('Triggered readyforpreroll event (Preroll)'); player.trigger('readyforpreroll'); }); } /* * adscanceled cancels all ads for the source. Play content now. */ ; _proto.onAdsCanceled = function onAdsCanceled(player) { var _this2 = this; player.ads.debug('adscanceled (Preroll)'); this.afterLoadStart(function () { _this2.resumeAfterNoPreroll(player); }); } /* * An ad error occured. Play content instead. */ ; _proto.onAdsError = function onAdsError(player) { var _this3 = this; videojs.log('adserror (Preroll)'); // In the future, we may not want to do this automatically. // Ad plugins should be able to choose to continue the ad break // if there was an error. if (this.inAdBreak()) { player.ads.endLinearAdMode(); } else { this.afterLoadStart(function () { _this3.resumeAfterNoPreroll(player); }); } } /* * Ad plugin invoked startLinearAdMode, the ad break starts now. */ ; _proto.startLinearAdMode = function startLinearAdMode() { var player = this.player; if (this.adsReady && !player.ads.inAdBreak() && !this.isContentResuming()) { this.clearTimeout(player); player.ads.adType = 'preroll'; this.waitingForAdBreak = false; obj$1.start(player); // We don't need to block play calls anymore player.ads._shouldBlockPlay = false; } else { videojs.log.warn('Unexpected startLinearAdMode invocation (Preroll)'); } } /* * An ad has actually started playing. * Remove the loading spinner. */ ; _proto.onAdStarted = function onAdStarted(player) { player.removeClass('vjs-ad-loading'); } /* * Ad plugin invoked endLinearAdMode, the ad break ends now. */ ; _proto.endLinearAdMode = function endLinearAdMode() { var player = this.player; if (this.inAdBreak()) { player.removeClass('vjs-ad-loading'); player.addClass('vjs-ad-content-resuming'); this.contentResuming = true; obj$1.end(player); } } /* * Ad skipped by ad plugin. Play content instead. */ ; _proto.skipLinearAdMode = function skipLinearAdMode() { var _this4 = 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)'); _this4.resumeAfterNoPreroll(player); }); } } /* * Prerolls took too long! Play content instead. */ ; _proto.onAdTimeout = function onAdTimeout(player) { var _this5 = this; this.afterLoadStart(function () { player.ads.debug('adtimeout (Preroll)'); _this5.resumeAfterNoPreroll(player); }); } /* * Check if nopreroll event was too late before handling it. */ ; _proto.onNoPreroll = function onNoPreroll(player) { if (player.ads.inAdBreak() || this.isContentResuming()) { videojs.log.warn('Unexpected nopreroll event (Preroll)'); } else { this.noPreroll(); } }; _proto.resumeAfterNoPreroll = function resumeAfterNoPreroll(player) { // Resume to content and unblock play as there is no preroll ad this.contentResuming = true; player.ads._shouldBlockPlay = false; this.cleanupPartial(player); // Play the content if we had requested play or we paused on 'contentupdate' // and we haven't played yet. This happens if there was no preroll or if it // errored, timed out, etc. Otherwise snapshot restore would play. if (player.ads._playRequested || player.ads._pausedOnContentupdate) { if (player.paused()) { player.ads.debug('resumeAfterNoPreroll: attempting to resume playback (Preroll)'); var playPromise = player.play(); if (playPromise && playPromise.then) { playPromise.then(null, function (e) {}); } } else { player.ads.debug('resumeAfterNoPreroll: already playing (Preroll)'); player.trigger('play'); player.trigger('playing'); } } } /* * Cleanup timeouts and spinner. */ ; _proto.cleanup = function cleanup(player) { if (!player.ads._hasThereBeenALoadStartDuringPlayerLife) { videojs.log.warn('Leaving Preroll state before loadstart event can cause issues.'); } this.cleanupPartial(player); } /* * Performs cleanup tasks without depending on a state transition. This is * used mainly in cases where a preroll failed. */ ; _proto.cleanupPartial = function cleanupPartial(player) { player.removeClass('vjs-ad-loading'); player.removeClass('vjs-ad-content-resuming'); this.clearTimeout(player); } /* * Clear the preroll timeout and nulls out the pointer. */ ; _proto.clearTimeout = function clearTimeout(player) { player.clearTimeout(this._timeout); this._timeout = null; }; return Preroll; }(AdState$1); States.registerState('Preroll', Preroll); var ContentState$2 = States.getState('ContentState'); /* * 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 = /*#__PURE__*/ function (_ContentState) { _inheritsLoose(BeforePreroll, _ContentState); function BeforePreroll() { return _ContentState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ BeforePreroll._getName = function _getName() { return 'BeforePreroll'; } /* * For state transitions to work correctly, initialization should * happen here, not in a constructor. */ ; var _proto = BeforePreroll.prototype; _proto.init = function init(player) { this.adsReady = false; this.shouldResumeToContent = false; // Content playback should be blocked by callPlay() middleware if the allowVjsAutoplay // option hasn't been provided and autoplay is not desired. player.ads._shouldBlockPlay = player.ads.settings.allowVjsAutoplay ? !player.autoplay() : true; } /* * The ad plugin may trigger adsready before the play request. If so, * we record that adsready already happened so the Preroll state will know. */ ; _proto.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. */ ; _proto.onPlay = function onPlay(player) { var Preroll = States.getState('Preroll'); player.ads.debug('Received play event (BeforePreroll)'); // Check for prerolls this.transitionTo(Preroll, this.adsReady, this.shouldResumeToContent); } /* * All ads for the entire video are canceled. */ ; _proto.onAdsCanceled = function onAdsCanceled(player) { player.ads.debug('adscanceled (BeforePreroll)'); this.shouldResumeToContent = true; } /* * An ad error occured. Play content instead. */ ; _proto.onAdsError = function onAdsError() { this.player.ads.debug('adserror (BeforePreroll)'); this.shouldResumeToContent = true; } /* * If there is no preroll, don't wait for a play event to move forward. */ ; _proto.onNoPreroll = function onNoPreroll() { this.player.ads.debug('Skipping prerolls due to nopreroll event (BeforePreroll)'); this.shouldResumeToContent = true; } /* * Prerolls skipped by ad plugin. Play content instead. */ ; _proto.skipLinearAdMode = function skipLinearAdMode() { var player = this.player; player.trigger('adskip'); player.ads.debug('skipLinearAdMode (BeforePreroll)'); this.shouldResumeToContent = true; }; _proto.onContentChanged = function onContentChanged() { this.init(this.player); }; return BeforePreroll; }(ContentState$2); States.registerState('BeforePreroll', BeforePreroll); var AdState$2 = States.getState('AdState'); var Midroll = /*#__PURE__*/ function (_AdState) { _inheritsLoose(Midroll, _AdState); function Midroll() { return _AdState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ Midroll._getName = function _getName() { return 'Midroll'; } /* * Midroll breaks happen when the ad plugin calls startLinearAdMode, * which can happen at any time during content playback. */ ; var _proto = Midroll.prototype; _proto.init = function init(player) { player.ads.adType = 'midroll'; obj$1.start(player); player.addClass('vjs-ad-loading'); } /* * An ad has actually started playing. * Remove the loading spinner. */ ; _proto.onAdStarted = function onAdStarted(player) { player.removeClass('vjs-ad-loading'); } /* * Midroll break is done. */ ; _proto.endLinearAdMode = function endLinearAdMode() { var player = this.player; if (this.inAdBreak()) { this.contentResuming = true; player.addClass('vjs-ad-content-resuming'); player.removeClass('vjs-ad-loading'); obj$1.end(player); } } /* * End midroll break if there is an error. */ ; _proto.onAdsError = function onAdsError(player) { // In the future, we may not want to do this automatically. // Ad plugins should be able to choose to continue the ad break // if there was an error. if (this.inAdBreak()) { player.ads.endLinearAdMode(); } } /* * Cleanup CSS classes. */ ; _proto.cleanup = function cleanup(player) { player.removeClass('vjs-ad-loading'); player.removeClass('vjs-ad-content-resuming'); }; return Midroll; }(AdState$2); States.registerState('Midroll', Midroll); var AdState$3 = States.getState('AdState'); var Postroll = /*#__PURE__*/ function (_AdState) { _inheritsLoose(Postroll, _AdState); function Postroll() { return _AdState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ Postroll._getName = function _getName() { return 'Postroll'; } /* * For state transitions to work correctly, initialization should * happen here, not in a constructor. */ ; var _proto = Postroll.prototype; _proto.init = function init(player) { this.waitingForAdBreak = true; // 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 { this.resumeContent(player); var AdsDone = States.getState('AdsDone'); this.transitionTo(AdsDone); } } /* * Start the postroll if it's not too late. */ ; _proto.startLinearAdMode = function startLinearAdMode() { var player = this.player; if (!player.ads.inAdBreak() && !this.isContentResuming()) { player.ads.adType = 'postroll'; player.clearTimeout(this._postrollTimeout); this.waitingForAdBreak = false; obj$1.start(player); } else { videojs.log.warn('Unexpected startLinearAdMode invocation (Postroll)'); } } /* * An ad has actually started playing. * Remove the loading spinner. */ ; _proto.onAdStarted = function onAdStarted(player) { player.removeClass('vjs-ad-loading'); } /* * Ending a postroll triggers the ended event. */ ; _proto.endLinearAdMode = function endLinearAdMode() { var _this = this; var player = this.player; var AdsDone = States.getState('AdsDone'); if (this.inAdBreak()) { player.removeClass('vjs-ad-loading'); this.resumeContent(player); obj$1.end(player, function () { _this.transitionTo(AdsDone); }); } } /* * Postroll skipped, time to clean up. */ ; _proto.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(player); } } /* * Postroll timed out, time to clean up. */ ; _proto.onAdTimeout = function onAdTimeout(player) { player.ads.debug('Postroll abort (adtimeout)'); this.abort(player); } /* * Postroll errored out, time to clean up. */ ; _proto.onAdsError = function onAdsError(player) { player.ads.debug('Postroll abort (adserror)'); // In the future, we may not want to do this automatically. // Ad plugins should be able to choose to continue the ad break // if there was an error. if (player.ads.inAdBreak()) { player.ads.endLinearAdMode(); } else { this.abort(player); } } /* * Handle content change if we're not in an ad break. */ ; _proto.onContentChanged = function onContentChanged(player) { // Content resuming after Postroll. Content is paused // at this point, since it is done playing. if (this.isContentResuming()) { var BeforePreroll = States.getState('BeforePreroll'); this.transitionTo(BeforePreroll); // Waiting for postroll to start. Content is considered playing // at this point, since it had to be playing to start the postroll. } else if (!this.inAdBreak()) { var Preroll = States.getState('Preroll'); this.transitionTo(Preroll); } } /* * Wrap up if there is no postroll. */ ; _proto.onNoPostroll = function onNoPostroll(player) { if (!this.isContentResuming() && !this.inAdBreak()) { this.abort(player); } else { videojs.log.warn('Unexpected nopostroll event (Postroll)'); } }; _proto.resumeContent = function resumeContent(player) { this.contentResuming = true; player.addClass('vjs-ad-content-resuming'); } /* * Helper for ending Postrolls. In the future we may want to * refactor this class so that `cleanup` handles all of this. */ ; _proto.abort = function abort(player) { var AdsDone = States.getState('AdsDone'); this.resumeContent(player); player.removeClass('vjs-ad-loading'); this.transitionTo(AdsDone); } /* * Cleanup timeouts and state. */ ; _proto.cleanup = function cleanup(player) { player.removeClass('vjs-ad-content-resuming'); player.clearTimeout(this._postrollTimeout); player.ads._contentEnding = false; }; return Postroll; }(AdState$3); States.registerState('Postroll', Postroll); var ContentState$3 = States.getState('ContentState'); /* * This state represents content playback the first time through before * content ends. After content has ended once, we check for postrolls and * move on to the AdsDone state rather than returning here. */ var ContentPlayback = /*#__PURE__*/ function (_ContentState) { _inheritsLoose(ContentPlayback, _ContentState); function ContentPlayback() { return _ContentState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ ContentPlayback._getName = function _getName() { return 'ContentPlayback'; } /* * For state transitions to work correctly, initialization should * happen here, not in a constructor. */ ; var _proto = ContentPlayback.prototype; _proto.init = function init(player) { // Don't block calls to play in content playback player.ads._shouldBlockPlay = false; } /* * In the case of a timeout, adsready might come in late. This assumes the behavior * that if an ad times out, it could still interrupt the content and start playing. * An ad plugin could behave otherwise by ignoring this event. */ ; _proto.onAdsReady = function onAdsReady(player) { player.ads.debug('Received adsready event (ContentPlayback)'); if (!player.ads.nopreroll_) { player.ads.debug('Triggered readyforpreroll event (ContentPlayback)'); player.trigger('readyforpreroll'); } } /* * Content ended before postroll checks. */ ; _proto.onReadyForPostroll = function onReadyForPostroll(player) { var Postroll = States.getState('Postroll'); player.ads.debug('Received readyforpostroll event'); this.transitionTo(Postroll); } /* * This is how midrolls start. */ ; _proto.startLinearAdMode = function startLinearAdMode() { var Midroll = States.getState('Midroll'); this.transitionTo(Midroll); }; return ContentPlayback; }(ContentState$3); States.registerState('ContentPlayback', ContentPlayback); var ContentState$4 = States.getState('ContentState'); /* * This state represents content playback when stitched ads are in play. */ var StitchedContentPlayback = /*#__PURE__*/ function (_ContentState) { _inheritsLoose(StitchedContentPlayback, _ContentState); function StitchedContentPlayback() { return _ContentState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ StitchedContentPlayback._getName = function _getName() { return 'StitchedContentPlayback'; } /* * For state transitions to work correctly, initialization should * happen here, not in a constructor. */ ; var _proto = StitchedContentPlayback.prototype; _proto.init = function init() { // Don't block calls to play in stitched ad players, ever. this.player.ads._shouldBlockPlay = false; } /* * Source change does not do anything for stitched ad players. * contentchanged does not fire during ad breaks, so we don't need to * worry about that. */ ; _proto.onContentChanged = function onContentChanged() { this.player.ads.debug("Received contentchanged event (" + this.constructor._getName() + ")"); } /* * This is how stitched ads start. */ ; _proto.startLinearAdMode = function startLinearAdMode() { var StitchedAdRoll = States.getState('StitchedAdRoll'); this.transitionTo(StitchedAdRoll); }; return StitchedContentPlayback; }(ContentState$4); States.registerState('StitchedContentPlayback', StitchedContentPlayback); var AdState$4 = States.getState('AdState'); var StitchedAdRoll = /*#__PURE__*/ function (_AdState) { _inheritsLoose(StitchedAdRoll, _AdState); function StitchedAdRoll() { return _AdState.apply(this, arguments) || this; } /* * Allows state name to be logged even after minification. */ StitchedAdRoll._getName = function _getName() { return 'StitchedAdRoll'; } /* * StitchedAdRoll breaks happen when the ad plugin calls startLinearAdMode, * which can happen at any time during content playback. */ ; var _proto = StitchedAdRoll.prototype; _proto.init = function init() { this.waitingForAdBreak = false; this.contentResuming = false; this.player.ads.adType = 'stitched'; obj$1.start(this.player); } /* * For stitched ads, there is no "content resuming" scenario, so a "playing" * event is not relevant. */ ; _proto.onPlaying = function onPlaying() {} /* * For stitched ads, there is no "content resuming" scenario, so a * "contentresumed" event is not relevant. */ ; _proto.onContentResumed = function onContentResumed() {} /* * When we see an "adended" event, it means that we are in a postroll that * has ended (because the media ended and we are still in an ad state). * * In these cases, we transition back to content mode and fire ended. */ ; _proto.onAdEnded = function onAdEnded() { this.endLinearAdMode(); this.player.trigger('ended'); } /* * StitchedAdRoll break is done. */ ; _proto.endLinearAdMode = function endLinearAdMode() { var StitchedContentPlayback = States.getState('StitchedContentPlayback'); obj$1.end(this.player); this.transitionTo(StitchedContentPlayback); }; return StitchedAdRoll; }(AdState$4); States.registerState('StitchedAdRoll', StitchedAdRoll); /* This main plugin file is responsible for the public API and enabling the features that live in in separate files. */ var isMiddlewareMediatorSupported$1 = obj.isMiddlewareMediatorSupported; var VIDEO_EVENTS = videojs.getTech('Html5').Events; // Default settings var defaults = { // Maximum amount of time in ms to wait to receive `adsready` from the ad // implementation after play has been requested. Ad implementations are // expected to load any dynamic libraries and make any requests to determine // ad policies for a video during this time. timeout: 5000, // Maximum amount of time in ms to wait for the ad implementation to start // linear ad mode after `readyforpreroll` has fired. This is in addition to // the standard timeout. prerollTimeout: undefined, // Maximum amount of time in ms to wait for the ad implementation to start // linear ad mode after `readyforpostroll` has fired. postrollTimeout: undefined, // When truthy, instructs the plugin to output additional information about // plugin state to the video.js log. On most devices, the video.js log is // the same as the developer console. debug: false, // Set this to true when using ads that are part of the content video stitchedAds: false, // Force content to be treated as live or not live // if not defined, the code will try to infer if content is live, // which can have limitations. contentIsLive: undefined, // If set to true, content will play muted behind ads on supported platforms. This is // to support ads on video metadata cuepoints during a live stream. It also results in // more precise resumes after ads during a live stream. liveCuePoints: true, // If set to true, callPlay middleware will not terminate the first play request in // BeforePreroll if the player intends to autoplay. This allows the manual autoplay // attempt made by video.js to resolve/reject naturally and trigger an 'autoplay-success' // or 'autoplay-failure' event with which other plugins can interface. allowVjsAutoplay: videojs.options.normalizeAutoplay || false }; var contribAdsPlugin = function contribAdsPlugin(options) { var player = this; // eslint-disable-line consistent-this var settings = videojs.mergeOptions(defaults, options); // Prefix all video element events during ad playback // if the video element emits ad-related events directly, // plugins that aren't ad-aware will break. prefixing allows // plugins that wish to handle ad events to do so while // avoiding the complexity for common usage var videoEvents = []; // dedupe event names VIDEO_EVENTS.concat(['firstplay', 'loadedalldata']).forEach(function (eventName) { if (videoEvents.indexOf(eventName) === -1) { videoEvents.push(eventName); } }); // Set up redispatching of player events player.on(videoEvents, redispatch); // Set up features to block content playback while waiting for ads. // Play middleware is only supported on later versions of video.js // and on desktop currently(as the user-gesture requirement on mobile // will disallow calling play once play blocking is lifted) // The middleware must also be registered outside of the plugin, // to avoid a middleware factory being created for each player if (!isMiddlewareMediatorSupported$1()) { initCancelContentPlay(player, settings.debug); } // If we haven't seen a loadstart after 5 seconds, the plugin was not initialized // correctly. player.setTimeout(function () { if (!player.ads._hasThereBeenALoadStartDuringPlayerLife && player.src() !== '') { videojs.log.error('videojs-contrib-ads has not seen a loadstart event 5 seconds ' + 'after being initialized, but a source is present. This indicates that ' + 'videojs-contrib-ads was initialized too late. It must be initialized ' + 'immediately after video.js in the same tick. As a result, some ads will not ' + 'play and some media events will be incorrect. For more information, see ' + 'http://videojs.github.io/videojs-contrib-ads/integrator/getting-started.html'); } }, 5000); // "vjs-has-started" should be present at the end of a video. This makes sure it's // always there. player.on('ended', function () { if (!player.hasClass('vjs-has-started')) { player.addClass('vjs-has-started'); } }); // video.js removes the vjs-waiting class on timeupdate. We want // to make sure this still happens during content restoration. player.on('contenttimeupdate', function () { player.removeClass('vjs-waiting'); }); // We now auto-play when an ad gets loaded if we're playing ads in the same video // element as the content. // The problem is that in IE11, we cannot play in addurationchange but in iOS8, we // cannot play from adcanplay. // This will prevent ad plugins from needing to do this themselves. player.on(['addurationchange', 'adcanplay'], function () { // We don't need to handle this for stitched ads because // linear ads in such cases are stitched into the content. if (player.ads.settings.stitchedAds) { return; } // Some techs may retrigger canplay after playback has begun. // So we want to procceed only if playback hasn't started. if (player.hasStarted()) { return; } if (player.ads.snapshot && player.currentSrc() === player.ads.snapshot.currentSrc) { return; } // If an ad isn't playing, don't try to play an ad. This could result from prefixed // events when the player is blocked by a preroll check, but there is no preroll. if (!player.ads.inAdBreak()) { return; } var playPromise = player.play(); if (playPromise && playPromise.catch) { playPromise.catch(function (error) { videojs.log.warn('Play promise rejected when playing ad', error); }); } }); player.on('nopreroll', function () { player.ads.debug('Received nopreroll event'); player.ads.nopreroll_ = true; }); player.on('nopostroll', function () { player.ads.debug('Received nopostroll event'); player.ads.nopostroll_ = true; }); // Restart the cancelContentPlay process. player.on('playing', function () { player.ads._cancelledPlay = false; player.ads._pausedOnContentupdate = false; }); // Keep track of whether a play event has happened player.on('play', function () { player.ads._playRequested = true; }); player.one('loadstart', function () { player.ads._hasThereBeenALoadStartDuringPlayerLife = true; }); player.on('loadeddata', function () { player.ads._hasThereBeenALoadedData = true; }); player.on('loadedmetadata', function () { player.ads._hasThereBeenALoadedMetaData = true; }); // Replace the plugin constructor with the ad namespace player.ads = getAds(player); player.ads.settings = settings; // Set the stitched ads state. This needs to happen before the `_state` is // initialized below - BeforePreroll needs to know whether contrib-ads is // playing stitched ads or not. // The setter is deprecated, so this does not use it. // But first, cast to boolean. settings.stitchedAds = !!settings.stitchedAds; if (settings.stitchedAds) { player.ads._state = new (States.getState('StitchedContentPlayback'))(player); } else { player.ads._state = new (States.getState('BeforePreroll'))(player); } player.ads._state.init(player); player.ads.cueTextTracks = cueTextTracks; player.ads.adMacroReplacement = adMacroReplacement.bind(player); // Start sending contentupdate and contentchanged events for this player initializeContentupdate(player); // Global contentchanged handler for resetting plugin state player.on('contentchanged', player.ads.reset); // A utility method for textTrackChangeHandler to define the conditions // when text tracks should be disabled. // Currently this includes: // - on iOS with native text tracks, during an ad playing var shouldDisableTracks = function shouldDisableTracks() { // If the platform matches iOS with native text tracks // and this occurs during ad playback, we should disable tracks again. // If shouldPlayContentBehindAd, no special handling is needed. return !player.ads.shouldPlayContentBehindAd(player) && player.ads.inAdBreak() && player.tech_.featuresNativeTextTracks && videojs.browser.IS_IOS && // older versions of video.js did not use an emulated textTrackList !Array.isArray(player.textTracks()); }; /* * iOS Safari will change caption mode to 'showing' if a user previously * turned captions on manually for that video source, so this TextTrackList * 'change' event handler will re-disable them in case that occurs during ad playback */ var textTrackChangeHandler = function textTrackChangeHandler() { var textTrackList = player.textTracks(); if (shouldDisableTracks()) { // We must double check all tracks for (var i = 0; i < textTrackList.length; i++) { var track = textTrackList[i]; if (track.mode === 'showing') { track.mode = 'disabled'; } } } }; // Add the listener to the text track list player.ready(function () { player.textTracks().addEventListener('change', textTrackChangeHandler); }); // Event handling for the current state. player.on(['play', 'playing', 'ended', 'adsready', 'adscanceled', 'adskip', 'adserror', 'adtimeout', 'adended', 'ads-ad-started', 'contentchanged', 'dispose', 'contentresumed', 'readyforpostroll', 'nopreroll', 'nopostroll'], function (e) { player.ads._state.handleEvent(e.type); }); // Clear timeouts and handlers when player is disposed player.on('dispose', function () { player.ads.reset(); player.textTracks().removeEventListener('change', textTrackChangeHandler); }); // Listen to TCF changes listenToTcf(); // Can be called for testing, or if the TCF CMP has loaded late player.ads.listenToTcf = listenToTcf; }; // Expose the contrib-ads version before it is initialized. Will be replaced // after initialization in ads.js contribAdsPlugin.VERSION = version; // Attempt to register the plugin, if we can. register(contribAdsPlugin); return contribAdsPlugin; }));