UNPKG

videojs-contrib-ads

Version:

A framework that provides common functionality needed by video advertisement libraries working with video.js.

1,841 lines (1,424 loc) 153 kB
(function (QUnit$1,videojs$1) { 'use strict'; QUnit$1 = QUnit$1 && QUnit$1.hasOwnProperty('default') ? QUnit$1['default'] : QUnit$1; videojs$1 = videojs$1 && videojs$1.hasOwnProperty('default') ? videojs$1['default'] : videojs$1; 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 _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 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; }; 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$1.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$1.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$1.log.warn('Unexpected startLinearAdMode invocation ' + '(State via ' + this.constructor.name + ')'); }; State.prototype.endLinearAdMode = function endLinearAdMode() { videojs$1.log.warn('Unexpected endLinearAdMode invocation ' + '(State via ' + this.constructor.name + ')'); }; State.prototype.skipLinearAdMode = function skipLinearAdMode() { videojs$1.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); } var CancelContentPlay = (Object.freeze || Object)({ 'default': cancelContentPlay }); /* 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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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$1.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.debug('adscanceled (BeforePreroll)'); this.transitionTo(ContentPlayback); }; /* * An ad error occured. Play content instead. */ BeforePreroll.prototype.onAdsError = function onAdsError() { this.transitionTo(ContentPlayback); }; /* * If there is no preroll, don't wait for a play event to move forward. */ BeforePreroll.prototype.onNoPreroll = function onNoPreroll() { this.player.ads.debug('Skipping prerolls due to nopreroll event (BeforePreroll)'); this.transitionTo(ContentPlayback); }; /* * Prerolls skipped by integration. Play content instead. */ BeforePreroll.prototype.skipLinearAdMode = function skipLinearAdMode() { var player = this.player; player.trigger('adskip'); this.transitionTo(ContentPlayback); }; /* * Content source change before preroll is currently not handled. When * developed, this is where to start. */ BeforePreroll.prototype.onContentChanged = function onContentChanged() {}; return BeforePreroll; }(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 = function (_ContentState) { inherits(ContentPlayback, _ContentState); function ContentPlayback() { classCallCheck(this, ContentPlayback); return possibleConstructorReturn(this, _ContentState.apply(this, arguments)); } ContentPlayback.prototype.init = function init(player) { // Play the content if cancelContentPlay happened 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.paused() && (player.ads._cancelledPlay || player.ads._pausedOnContentupdate)) { player.play(); } }; /* * 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 integration could behave otherwise by ignoring this event. */ ContentPlayback.prototype.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. */ ContentPlayback.prototype.onContentEnded = function onContentEnded(player) { player.ads.debug('Received contentended event'); this.transitionTo(Postroll); }; /* * This is how midrolls start. */ ContentPlayback.prototype.startLinearAdMode = function startLinearAdMode() { this.transitionTo(Midroll); }; return ContentPlayback; }(ContentState); var AdsDone = function (_ContentState) { inherits(AdsDone, _ContentState); function AdsDone() { classCallCheck(this, AdsDone); return possibleConstructorReturn(this, _ContentState.apply(this, arguments)); } AdsDone.prototype.init = function init(player) { // From now on, `ended` events won't be redispatched player.ads._contentHasEnded = true; }; /* * Midrolls do not play after ads are done. */ AdsDone.prototype.startLinearAdMode = function startLinearAdMode() { videojs$1.log.warn('Unexpected startLinearAdMode invocation (AdsDone)'); }; return AdsDone; }(ContentState); /* * This file is necessary to avoid this rollup issue: * https://github.com/rollup/rollup/issues/1089 */ /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('AdState', { beforeEach: function beforeEach() { var _this = this; this.player = { ads: {} }; this.adState = new AdState(this.player); this.adState.transitionTo = function (newState) { _this.newState = newState.name; }; } }); QUnit$1.test('does not start out with content resuming', function (assert) { assert.equal(this.adState.contentResuming, false); }); QUnit$1.test('is an ad state', function (assert) { assert.equal(this.adState.isAdState(), true); }); QUnit$1.test('transitions to ContentPlayback on playing if content resuming', function (assert) { this.adState.contentResuming = true; this.adState.onPlaying(); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('doesn\'t transition on playing if content not resuming', function (assert) { this.adState.onPlaying(); assert.equal(this.newState, undefined, 'no transition'); }); QUnit$1.test('transitions to ContentPlayback on contentresumed if content resuming', function (assert) { this.adState.contentResuming = true; this.adState.onContentResumed(); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('doesn\'t transition on contentresumed if content not resuming', function (assert) { this.adState.onContentResumed(); assert.equal(this.newState, undefined, 'no transition'); }); QUnit$1.test('can check if content is resuming', function (assert) { assert.equal(this.adState.isContentResuming(), false, 'not resuming'); this.adState.contentResuming = true; assert.equal(this.adState.isContentResuming(), true, 'resuming'); }); QUnit$1.test('can check if in ad break', function (assert) { assert.equal(this.adState.inAdBreak(), false, 'not in ad break'); this.player.ads._inLinearAdMode = true; assert.equal(this.adState.inAdBreak(), true, 'in ad break'); }); /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('ContentState', { beforeEach: function beforeEach() { var _this = this; this.player = { ads: { debug: function debug() {} } }; this.contentState = new ContentState(this.player); this.contentState.transitionTo = function (newState) { _this.newState = newState.name; }; } }); QUnit$1.test('is not an ad state', function (assert) { assert.equal(this.contentState.isAdState(), false); }); QUnit$1.test('handles content changed when not playing', function (assert) { this.player.paused = function () { return true; }; this.player.pause = sinon.stub(); this.contentState.onContentChanged(this.player); assert.equal(this.newState, 'BeforePreroll'); assert.equal(this.player.pause.callCount, 0, 'did not pause player'); assert.ok(!this.player.ads._pausedOnContentupdate, 'did not set _pausedOnContentupdate'); }); QUnit$1.test('handles content changed when playing', function (assert) { this.player.paused = function () { return false; }; this.player.pause = sinon.stub(); this.contentState.onContentChanged(this.player); assert.equal(this.newState, 'Preroll'); assert.equal(this.player.pause.callCount, 1, 'paused player'); assert.equal(this.player.ads._pausedOnContentupdate, true, 'set _pausedOnContentupdate'); }); /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('State', { beforeEach: function beforeEach() { this.player = { ads: { debug: function debug() {} } }; this.state = new State(this.player); } }); QUnit$1.test('sets this.player', function (assert) { assert.equal(this.state.player, this.player); }); QUnit$1.test('can transition to another state', function (assert) { var mockStateInit = false; var MockState = function () { function MockState() { classCallCheck(this, MockState); } MockState.prototype.init = function init() { mockStateInit = true; }; return MockState; }(); this.state.cleanup = sinon.stub(); this.state.transitionTo(MockState); assert.ok(this.state.cleanup.calledOnce, 'cleaned up old state'); assert.equal(this.player.ads._state.constructor.name, 'MockState', 'set ads._state'); assert.equal(mockStateInit, true, 'initialized new state'); }); QUnit$1.test('throws error if isAdState is not implemented', function (assert) { var error = void 0; try { this.state.isAdState(); } catch (e) { error = e; } assert.equal(error.message, 'isAdState unimplemented for State'); }); QUnit$1.test('is not resuming content by default', function (assert) { assert.equal(this.state.isContentResuming(), false); }); QUnit$1.test('is not in an ad break by default', function (assert) { assert.equal(this.state.inAdBreak(), false); }); QUnit$1.test('handles events', function (assert) { this.state.onPlay = sinon.stub(); this.state.handleEvent('play'); assert.ok(this.state.onPlay.calledOnce); }); /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('AdsDone', { beforeEach: function beforeEach() { this.player = { ads: {} }; this.adsDone = new AdsDone(this.player); } }); QUnit$1.test('sets _contentHasEnded on init', function (assert) { this.adsDone.init(this.player); assert.equal(this.player.ads._contentHasEnded, true, 'content has ended'); }); QUnit$1.test('does not play midrolls', function (assert) { this.adsDone.transitionTo = sinon.spy(); this.adsDone.init(this.player); this.adsDone.startLinearAdMode(); assert.equal(this.adsDone.transitionTo.callCount, 0, 'no transition'); }); /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('BeforePreroll', { beforeEach: function beforeEach() { var _this = this; this.events = []; this.player = { ads: { debug: function debug() {} }, setTimeout: function setTimeout() {}, trigger: function trigger(event) { _this.events.push(event); } }; this.beforePreroll = new BeforePreroll(this.player); this.beforePreroll.transitionTo = function (newState, arg) { _this.newState = newState.name; _this.transitionArg = arg; }; this.cancelContentPlayStub = sinon.stub(CancelContentPlay, 'cancelContentPlay'); }, afterEach: function afterEach() { this.cancelContentPlayStub.restore(); } }); QUnit$1.test('transitions to Preroll (adsready first)', function (assert) { this.beforePreroll.init(); assert.equal(this.beforePreroll.adsReady, false); this.beforePreroll.onAdsReady(this.player); assert.equal(this.beforePreroll.adsReady, true); this.beforePreroll.onPlay(this.player); assert.equal(this.newState, 'Preroll'); assert.equal(this.transitionArg, true); }); QUnit$1.test('transitions to Preroll (play first)', function (assert) { this.beforePreroll.init(); assert.equal(this.beforePreroll.adsReady, false); this.beforePreroll.onPlay(this.player); assert.equal(this.newState, 'Preroll'); assert.equal(this.transitionArg, false); }); QUnit$1.test('cancels ads', function (assert) { this.beforePreroll.init(); this.beforePreroll.onAdsCanceled(this.player); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('transitions to content playback on error', function (assert) { this.beforePreroll.init(); this.beforePreroll.onAdsError(this.player); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('has no preroll', function (assert) { this.beforePreroll.init(); this.beforePreroll.onNoPreroll(this.player); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('skips the preroll', function (assert) { this.beforePreroll.init(); this.beforePreroll.skipLinearAdMode(); assert.equal(this.events[0], 'adskip'); assert.equal(this.newState, 'ContentPlayback'); }); QUnit$1.test('does nothing on content change', function (assert) { this.beforePreroll.init(); this.beforePreroll.onContentChanged(this.player); assert.equal(this.newState, undefined); }); /* * These tests are intended to be isolated unit tests for one state with all * other modules mocked. */ QUnit$1.module('ContentPlayback', { beforeEach: function beforeEach() { var _this = this; this.events = []; this.playTriggered = false; this.player = { paused: function paused() { return false; }, play: function play() { _this.playTriggered = true; }, trigger: function trigger(event) { _this.events.push(event); }, ads: { debug: function debug() {} } }; this.contentPlayback = new ContentPlayback(this.player); this.contentPlayback.transitionTo = function (newState) { _this.newState = newState.name; }; } }); QUnit$1.test('only plays on init on correct conditions', function (assert) { this.player.paused = function () { return false; }; this.player.ads._cancelledPlay = false; this.player.ads._pausedOnContentupdate = false; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, false); this.player.paused = function () { return true; }; this.player.ads._cancelledPlay = false; this.player.ads._pausedOnContentupdate = false; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, false); this.player.paused = function () { return false; }; this.player.ads._cancelledPlay = true; this.player.ads._pausedOnContentupdate = false; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, false); this.player.paused = function () { return false; }; this.player.ads._cancelledPlay = false; this.player.ads._pausedOnContentupdate = true; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, false); this.player.paused = function () { return true; }; this.player.ads._cancelledPlay = true; this.player.ads._pausedOnContentupdate = false; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, true); this.player.paused = function () { return true; }; this.player.ads._cancelledPlay = false; this.player.ads._pausedOnContentupdate = true; this.contentPlayback.init(this.player); assert.equal(this.playTriggered, true); }); QUnit$1.test('adsready triggers readyforpreroll', function (assert) { this.contentPlayback.init(this.player); this.contentPlayback.onAdsReady(this.player); assert.equal(this.events[0], 'readyforpreroll'); }); QUnit$1.test('no readyforpreroll if nopreroll_', function (assert) { this.player.ads.nopreroll_ = true; this.contentPlayback.init(this.player); this.contentPlayback.onAdsReady(this.player); assert.equal(this.events.length, 0, 'no events triggered'); }); QUnit$1.test('transitions to Postroll on contentended', function (assert) { this.contentPlayback.init(this.player, false); this.contentPlayback.onContentEnded(this.player); assert.equal(this.newState, 'Postroll', 'transitioned to Postroll'); }); QUnit$1.test('transitions to Midroll on startlinearadmode', function (assert) { this.contentPlayback.init(this.player, false); this.contentPlayback.startLinearAdMode(); assert.equal(this.newState, 'Midroll', 'transitioned to Midroll'); }); /* * These tests are intended t