UNPKG

videojs-contrib-ads

Version:

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

1,364 lines (1,061 loc) 86.2 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ (function (global){ if (typeof window !== "undefined") { module.exports = window; } else if (typeof global !== "undefined") { module.exports = global; } else if (typeof self !== "undefined"){ module.exports = self; } else { module.exports = {}; } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],2:[function(require,module,exports){ 'use strict'; exports.__esModule = true; exports['default'] = redispatch; /* 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 an ad 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. */ // Stop propogation for an event var cancelEvent = function cancelEvent(player, event) { // Pretend we called stopImmediatePropagation because we want the native // element events to continue propagating event.isImmediatePropagationStopped = function () { return true; }; event.cancelBubble = true; event.isPropagationStopped = function () { return true; }; }; // Stop propogation for an event, then send a new event with the type of the original // event with the given prefix added. var prefixEvent = function prefixEvent(player, prefix, event) { cancelEvent(player, event); player.trigger({ type: prefix + event.type, state: player.ads.state, originalEvent: 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) { // We do a quick play/pause before we check for prerolls. This creates a "playing" // event. This conditional block prefixes that event so it's "adplaying" if it // happens while we're in the "preroll?" state. Not every browser is in the // "preroll?" state for this event, so the following browsers come through here: // * iPad // * iPhone // * Android // * Safari // This is too soon to check videoElementRecycled because there is no snapshot // yet. We rely on the coincidence that all browsers for which // videoElementRecycled would be true also happen to send their initial playing // event during "preroll?" if (event.type === 'playing' && this.ads.state === 'preroll?') { prefixEvent(this, 'ad', event); // Here we send "adplaying" for browsers that send their initial "playing" event // (caused by the the initial play/pause) during the "ad-playback" state. // The following browsers come through here: // * Chrome // * IE11 // If the ad plays in the content tech (aka videoElementRecycled) there will be // another playing event when the ad starts. We check videoElementRecycled to // avoid a second adplaying event. Thankfully, at this point a snapshot exists // so we can safely check videoElementRecycled. } else if (event.type === 'playing' && this.ads.state === 'ad-playback' && !this.ads.videoElementRecycled()) { prefixEvent(this, 'ad', event); // If the ad takes a long time to load, "playing" caused by play/pause can happen // during "ads-ready?" instead of "preroll?" or "ad-playback", skipping the // other conditions that would normally catch it } else if (event.type === 'playing' && this.ads.state === 'ads-ready?') { prefixEvent(this, 'ad', event); // When an ad is playing in content tech, we would normally prefix // "playing" with "ad" to send "adplaying". However, when we did a play/pause // before the preroll, we already sent "adplaying". This condition prevents us // from sending another. } else if (event.type === 'playing' && this.ads.state === 'ad-playback' && this.ads.videoElementRecycled()) { cancelEvent(this, event); return; // When ad is playing in content tech, prefix everything with "ad". // This block catches many events such as emptied, play, timeupdate, and ended. } else if (this.ads.state === 'ad-playback') { if (this.ads.videoElementRecycled() || this.ads.stitchedAds()) { prefixEvent(this, 'ad', event); } // Send contentended if ended happens during content. // We will make sure an ended event is sent after postrolls. } else if (this.ads.state === 'content-playback' && event.type === 'ended') { prefixEvent(this, 'content', event); // Event prefixing during content resuming is complicated } else if (this.ads.state === 'content-resuming') { // This does not happen during normal circumstances. I wasn't able to reproduce // it, but the working theory is that it handles cases where restoring the // snapshot takes a long time, such as in iOS7 and older Firefox. if (this.ads.snapshot && this.currentSrc() !== this.ads.snapshot.currentSrc) { // Don't prefix `loadstart` event if (event.type === 'loadstart') { return; } // All other events get "content" prefix return prefixEvent(this, 'content', event); // Content resuming after postroll } else if (this.ads.snapshot && this.ads.snapshot.ended) { // Don't prefix `pause` and `ended` events // They don't always happen during content-resuming, but they might. // It seems to happen most often on iOS and Android. if (event.type === 'pause' || event.type === 'ended') { return; } // All other events get "content" prefix return prefixEvent(this, 'content', event); } // Content resuming after preroll or midroll // Events besides "playing" get "content" prefix if (event.type !== 'playing') { prefixEvent(this, 'content', event); } } } },{}],3:[function(require,module,exports){ (function (global){ 'use strict'; exports.__esModule = true; exports.getPlayerSnapshot = getPlayerSnapshot; exports.restorePlayerSnapshot = restorePlayerSnapshot; var _window = require('global/window'); var _window2 = _interopRequireDefault(_window); var _video = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null); var _video2 = _interopRequireDefault(_video); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /** * 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 */ /* The snapshot feature is responsible for saving the player state before an ad, then restoring the player state after an ad. */ function getPlayerSnapshot(player) { var currentTime = void 0; if (_video2['default'].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 remoteTracks = player.remoteTextTracks ? player.remoteTextTracks() : []; var tracks = player.textTracks ? player.textTracks() : []; var suppressedRemoteTracks = []; 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 < remoteTracks.length; i++) { var track = remoteTracks[i]; suppressedRemoteTracks.push({ track: track, mode: track.mode }); track.mode = 'disabled'; } snapshotObject.suppressedRemoteTracks = suppressedRemoteTracks; 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 suppressedRemoteTracks = snapshotObject.suppressedRemoteTracks; var suppressedTracks = snapshotObject.suppressedTracks; var trackSnapshot = void 0; var restoreTracks = function restoreTracks() { for (var i = 0; i < suppressedRemoteTracks.length; i++) { trackSnapshot = suppressedRemoteTracks[i]; trackSnapshot.track.mode = trackSnapshot.mode; } for (var _i2 = 0; _i2 < suppressedTracks.length; _i2++) { trackSnapshot = suppressedTracks[_i2]; trackSnapshot.track.mode = trackSnapshot.mode; } }; // finish restoring the playback state var resume = function resume() { var currentTime = void 0; if (_video2['default'].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(); } }; // 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--) { _window2['default'].setTimeout(tryToResume, 50); } else { try { resume(); } catch (e) { _video2['default'].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); // if the src changed for ad playback, reset it player.src({ src: snapshotObject.currentSrc, type: snapshotObject.type }); // safari requires a call to `load` to pick up a changed source player.load(); // 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 (!player.ended() || !snapshotObject.ended) { // if we didn't change the src, just restore the tracks restoreTracks(); // the src didn't change and this wasn't a postroll // just resume playback at the current time. player.play(); } } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"global/window":1}],4:[function(require,module,exports){ 'use strict'; (function (window, QUnit) { var timerExists = function timerExists(env, keyOrId) { var timerId = _.isNumber(keyOrId) ? keyOrId : env.player.ads[String(keyOrId)]; return env.clock.timers.hasOwnProperty(String(timerId)); }; QUnit.module('Ad Framework', window.sharedModuleHooks()); QUnit.test('begins in content-set', function (assert) { assert.expect(1); assert.strictEqual(this.player.ads.state, 'content-set'); }); QUnit.test('pauses to wait for prerolls when the plugin loads before play', function (assert) { var spy = sinon.spy(this.player, 'pause'); assert.expect(1); this.player.paused = function () { return false; }; this.player.trigger('adsready'); this.player.trigger('play'); this.clock.tick(1); this.player.trigger('play'); this.clock.tick(1); assert.strictEqual(spy.callCount, 2, 'play attempts are paused'); }); QUnit.test('pauses to wait for prerolls when the plugin loads after play', function (assert) { var pauseSpy; assert.expect(1); this.player.paused = function () { return false; }; pauseSpy = sinon.spy(this.player, 'pause'); this.player.trigger('play'); this.clock.tick(1); this.player.trigger('play'); this.clock.tick(1); assert.equal(pauseSpy.callCount, 2, 'play attempts are paused'); }); QUnit.test('stops canceling play events when an ad is playing', function (assert) { var setTimeoutSpy = sinon.spy(window, 'setTimeout'); assert.expect(10); // Throughout this test, we check both that the expected timeouts are // populated on the `clock` _and_ that `setTimeout` has been called the // expected number of times. assert.notOk(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` does not exist'); assert.notOk(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` does not exist'); this.player.trigger('play'); assert.strictEqual(setTimeoutSpy.callCount, 2, 'two timers were created (`cancelPlayTimeout` and `adTimeoutTimeout`)'); assert.ok(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` exists'); assert.ok(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` exists'); this.player.trigger('adsready'); assert.strictEqual(setTimeoutSpy.callCount, 3, '`adTimeoutTimeout` was re-scheduled'); assert.ok(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` exists'); this.clock.tick(1); this.player.trigger('adstart'); assert.strictEqual(this.player.ads.state, 'ad-playback', 'ads are playing'); assert.notOk(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` no longer exists'); assert.notOk(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` no longer exists'); window.setTimeout.restore(); }); QUnit.test('adstart is fired before a preroll', function (assert) { var spy = sinon.spy(); assert.expect(1); this.player.on('adstart', spy); this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); assert.strictEqual(spy.callCount, 1, 'a preroll triggers adstart'); }); QUnit.test('player has the .vjs-has-started class once a preroll begins', function (assert) { assert.expect(1); this.player.trigger('adsready'); // This is a bit of a hack in order to not need the test to be async. this.player.tech_.trigger('play'); this.player.ads.startLinearAdMode(); assert.ok(this.player.hasClass('vjs-has-started'), 'player has .vjs-has-started class'); }); QUnit.test('moves to content-playback after a preroll', function (assert) { assert.expect(2); this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); this.player.ads.endLinearAdMode(); assert.strictEqual(this.player.ads.state, 'content-resuming', 'the state is content-resuming'); this.player.trigger('playing'); assert.strictEqual(this.player.ads.state, 'content-playback', 'the state is content-resuming'); }); QUnit.test('moves to ad-playback if a midroll is requested', function (assert) { assert.expect(1); this.player.trigger('adsready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback', 'the state is ad-playback'); }); QUnit.test('moves to content-playback if the preroll times out', function (assert) { this.player.trigger('adsready'); this.player.trigger('play'); this.player.trigger('adtimeout'); assert.strictEqual(this.player.ads.state, 'content-playback', 'the state is content-playback'); }); QUnit.test('waits for adsready if play is received first', function (assert) { assert.expect(1); this.player.trigger('play'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'preroll?', 'the state is preroll?'); }); QUnit.test('moves to content-playback if a plugin does not finish initializing', function (assert) { this.player.trigger('play'); this.player.trigger('adtimeout'); assert.strictEqual(this.player.ads.state, 'content-playback', 'the state is content-playback'); }); QUnit.test('calls start immediately on play when ads are ready', function (assert) { var spy = sinon.spy(); assert.expect(1); this.player.on('readyforpreroll', spy); this.player.trigger('adsready'); this.player.trigger('play'); assert.strictEqual(spy.callCount, 1, 'readyforpreroll was fired'); }); QUnit.test('adds the ad-mode class when a preroll plays', function (assert) { var el; assert.expect(1); this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); el = this.player.el(); assert.ok(this.player.hasClass('vjs-ad-playing'), 'the ad class should be in "' + el.className + '"'); }); QUnit.test('removes the ad-mode class when a preroll finishes', function (assert) { var el; this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); this.player.ads.endLinearAdMode(); el = this.player.el(); assert.notOk(this.player.hasClass('vjs-ad-playing'), 'the ad class should not be in "' + el.className + '"'); assert.strictEqual(this.player.ads.triggerevent, 'adend', 'triggerevent for content-resuming should have been adend'); this.player.trigger('playing'); }); QUnit.test('adds a class while waiting for an ad plugin to load', function (assert) { var el; assert.expect(1); this.player.trigger('play'); el = this.player.el(); assert.ok(this.player.hasClass('vjs-ad-loading'), 'the ad loading class should be in "' + el.className + '"'); }); QUnit.test('adds a class while waiting for a preroll', function (assert) { var el; assert.expect(1); this.player.trigger('adsready'); this.player.trigger('play'); el = this.player.el(); assert.ok(this.player.hasClass('vjs-ad-loading'), 'the ad loading class should be in "' + el.className + '"'); }); QUnit.test('removes the loading class when the preroll begins', function (assert) { var el; assert.expect(1); this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); this.player.trigger('ads-ad-started'); el = this.player.el(); assert.notOk(this.player.hasClass('vjs-ad-loading'), 'there should be no ad loading class present in "' + el.className + '"'); }); QUnit.test('removes the loading class when the preroll times out', function (assert) { var el; this.player.trigger('adsready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('playing'); el = this.player.el(); assert.notOk(this.player.hasClass('vjs-ad-loading'), 'there should be no ad loading class present in "' + el.className + '"'); }); QUnit.test('starts the content video if there is no preroll', function (assert) { var spy = sinon.spy(this.player, 'play'); this.player.trigger('adsready'); this.player.trigger('play'); this.clock.tick(1); this.player.trigger('adtimeout'); assert.strictEqual(spy.callCount, 1, 'play is called once'); }); QUnit.test('removes the poster attribute so it does not flash between videos', function (assert) { this.video.poster = 'http://www.videojs.com/img/poster.jpg'; assert.ok(this.video.poster, 'the poster is present initially'); this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.video.poster, '', 'poster is removed'); }); QUnit.test('restores the poster attribute after ads have ended', function (assert) { this.video.poster = 'http://www.videojs.com/img/poster.jpg'; this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); this.player.ads.endLinearAdMode(); assert.ok(this.video.poster, 'the poster is restored'); this.player.trigger('playing'); }); QUnit.test('changing the src triggers "contentupdate"', function (assert) { var spy = sinon.spy(); assert.expect(1); this.player.on('contentupdate', spy); // set src and trigger synthetic 'loadstart' this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4'); this.player.trigger('loadstart'); assert.strictEqual(spy.callCount, 1, 'one contentupdate event fired'); }); QUnit.test('"contentupdate" should fire when src is changed in "content-resuming" state after postroll', function (assert) { var spy = sinon.spy(); assert.expect(2); this.player.on('contentupdate', spy); this.player.trigger('adsready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); this.player.trigger('adtimeout'); this.player.ads.snapshot.ended = true; // set src and trigger synthetic 'loadstart' this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4'); this.player.trigger('loadstart'); assert.strictEqual(spy.callCount, 1, 'one contentupdate event fired'); assert.strictEqual(this.player.ads.state, 'content-set', 'we are in the content-set state'); }); QUnit.test('"contentupdate" should fire when src is changed in "content-playback" state after postroll', function (assert) { var spy = sinon.spy(); assert.expect(2); this.player.on('contentupdate', spy); this.player.trigger('adsready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); this.player.trigger('adtimeout'); this.player.ads.snapshot.ended = true; this.player.trigger('ended'); // set src and trigger synthetic 'loadstart' this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4'); this.player.trigger('loadstart'); assert.strictEqual(spy.callCount, 1, 'one contentupdate event fired'); assert.strictEqual(this.player.ads.state, 'content-set', 'we are in the content-set state'); }); QUnit.test('changing src does not trigger "contentupdate" during ad playback', function (assert) { var spy = sinon.spy(); this.player.on('contentupdate', spy); // enter ad playback mode this.player.trigger('adsready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); // set src and trigger synthetic 'loadstart' this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4'); this.player.trigger('loadstart'); // finish playing ad this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 0, 'no contentupdate events fired'); }); QUnit.test('the `cancelPlayTimeout` timeout is cleared when exiting "preroll?"', function (assert) { var setTimeoutSpy = sinon.spy(window, 'setTimeout'); assert.expect(5); this.player.trigger('adsready'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'preroll?', 'the player is waiting for prerolls'); assert.strictEqual(setTimeoutSpy.callCount, 2, 'two timers were created (`cancelPlayTimeout` and `adTimeoutTimeout`)'); assert.ok(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` exists'); assert.ok(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` exists'); this.player.trigger('play'); this.player.trigger('play'); this.player.trigger('play'); assert.strictEqual(setTimeoutSpy.callCount, 2, 'no additional timers were created on subsequent "play" events'); window.setTimeout.restore(); }); QUnit.test('"adscanceled" allows us to transition from "content-set" to "content-playback"', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adscanceled'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('"adscanceled" allows us to transition from "ads-ready?" to "content-playback"', function (assert) { var setTimeoutSpy = sinon.spy(window, 'setTimeout'); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'ads-ready?'); assert.strictEqual(setTimeoutSpy.callCount, 2, 'two timers were created (`cancelPlayTimeout` and `adTimeoutTimeout`)'); assert.ok(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` exists'); assert.ok(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` exists'); this.player.trigger('adscanceled'); assert.strictEqual(this.player.ads.state, 'content-playback'); assert.notOk(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` was canceled'); window.setTimeout.restore(); }); QUnit.test('content is resumed on contentplayback if a user initiated play event is canceled', function (assert) { var playSpy = sinon.spy(this.player, 'play'); var setTimeoutSpy = sinon.spy(window, 'setTimeout'); assert.expect(8); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'ads-ready?'); assert.strictEqual(setTimeoutSpy.callCount, 2, 'two timers were created (`cancelPlayTimeout` and `adTimeoutTimeout`)'); assert.ok(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` exists'); assert.ok(timerExists(this, 'adTimeoutTimeout'), '`adTimeoutTimeout` exists'); this.clock.tick(1); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-playback'); assert.notOk(timerExists(this, 'cancelPlayTimeout'), '`cancelPlayTimeout` was canceled'); assert.strictEqual(playSpy.callCount, 1, 'a play event should be triggered once we enter "content-playback" state if on was canceled.'); }); QUnit.test('adserror in content-set transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adskip in content-set transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adskip'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adserror in ads-ready? transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'ads-ready?'); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adskip in ads-ready? transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'ads-ready?'); this.player.trigger('adskip'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adserror in ads-ready transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adskip in ads-ready transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('adskip'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adserror in preroll? transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'preroll?'); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adskip in preroll? transitions to content-playback', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'preroll?'); this.player.trigger('adskip'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adserror in postroll? transitions to content-playback and fires ended', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); assert.strictEqual(this.player.ads.state, 'postroll?'); this.player.ads.snapshot.ended = true; this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(this.player.ads.triggerevent, 'adserror', 'adserror should be the trigger event'); this.clock.tick(1); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adtimeout in postroll? transitions to content-playback and fires ended', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); assert.strictEqual(this.player.ads.state, 'postroll?'); this.player.ads.snapshot.ended = true; this.player.trigger('adtimeout'); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(this.player.ads.triggerevent, 'adtimeout', 'adtimeout should be the trigger event'); this.clock.tick(1); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adskip in postroll? transitions to content-playback and fires ended', function (assert) { assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); assert.strictEqual(this.player.ads.state, 'postroll?'); this.player.ads.snapshot.ended = true; this.player.trigger('adskip'); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(this.player.ads.triggerevent, 'adskip', 'adskip should be the trigger event'); this.clock.tick(1); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('an "ended" event is fired in "content-resuming" via a timeout if not fired naturally', function (assert) { var endedSpy = sinon.spy(); assert.expect(6); this.player.on('ended', endedSpy); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); assert.strictEqual(this.player.ads.state, 'postroll?'); this.player.ads.startLinearAdMode(); this.player.ads.snapshot.ended = true; this.player.ads.endLinearAdMode(); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(endedSpy.callCount, 0, 'we should not have gotten an ended event yet'); this.clock.tick(1000); assert.strictEqual(endedSpy.callCount, 1, 'we should have fired ended from the timeout'); }); QUnit.test('an "ended" event is not fired in "content-resuming" via a timeout if fired naturally', function (assert) { var endedSpy = sinon.spy(); assert.expect(6); this.player.on('ended', endedSpy); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.trigger('adtimeout'); this.player.trigger('ended'); assert.strictEqual(this.player.ads.state, 'postroll?'); this.player.ads.startLinearAdMode(); this.player.ads.snapshot.ended = true; this.player.ads.endLinearAdMode(); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(endedSpy.callCount, 0, 'we should not have gotten an ended event yet'); this.player.trigger('ended'); assert.strictEqual(endedSpy.callCount, 1, 'we should have fired ended from the timeout'); }); QUnit.test('adserror in ad-playback transitions to content-playback and triggers adend', function (assert) { var spy; assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); spy = sinon.spy(); this.player.on('adend', spy); this.player.trigger('adserror'); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(this.player.ads.triggerevent, 'adserror', 'The reason for content-resuming should have been adserror'); this.player.trigger('playing'); assert.strictEqual(this.player.ads.state, 'content-playback'); assert.strictEqual(spy.getCall(0).args[0].type, 'adend', 'adend should be fired when we enter content-playback from adserror'); }); QUnit.test('calling startLinearAdMode() when already in ad-playback does not trigger adstart', function (assert) { var spy = sinon.spy(); this.player.on('adstart', spy); assert.strictEqual(this.player.ads.state, 'content-set'); // go through preroll flow this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'preroll?'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback'); assert.strictEqual(spy.callCount, 1, 'adstart should have fired'); // add an extraneous start call this.player.ads.startLinearAdMode(); assert.strictEqual(spy.callCount, 1, 'adstart should not have fired'); // make sure subsequent adstarts trigger again on exit/re-enter this.player.ads.endLinearAdMode(); this.player.trigger('playing'); assert.strictEqual(this.player.ads.state, 'content-playback'); this.player.ads.startLinearAdMode(); assert.strictEqual(spy.callCount, 2, 'adstart should have fired'); }); QUnit.test('calling endLinearAdMode() in any state but ad-playback does not trigger adend', function (assert) { var spy; assert.expect(13); spy = sinon.spy(); this.player.on('adend', spy); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 0, 'adend should not have fired'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 0, 'adend should not have fired'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'preroll?'); this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 0, 'adend should not have fired'); this.player.trigger('adtimeout'); assert.strictEqual(this.player.ads.state, 'content-playback'); this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 0, 'adend should not have fired'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback'); this.player.ads.endLinearAdMode(); assert.strictEqual(spy.callCount, 1, 'adend should have fired'); this.player.trigger('playing'); assert.strictEqual(this.player.ads.state, 'content-playback'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback'); this.player.trigger('adserror'); assert.strictEqual(spy.callCount, 2, 'adend should have fired'); }); QUnit.test('skipLinearAdMode in ad-playback does not trigger adskip', function (assert) { var spy; spy = sinon.spy(); this.player.on('adskip', spy); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('adsready'); assert.strictEqual(this.player.ads.state, 'ads-ready'); this.player.trigger('play'); this.player.ads.startLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback'); this.player.ads.skipLinearAdMode(); assert.strictEqual(this.player.ads.state, 'ad-playback'); assert.strictEqual(spy.callCount, 0, 'adskip event should not trigger when skipLinearAdMode called in ad-playback state'); this.player.ads.endLinearAdMode(); assert.strictEqual(this.player.ads.state, 'content-resuming'); assert.strictEqual(this.player.ads.triggerevent, 'adend', 'The reason for content-resuming should have been adend'); this.player.trigger('playing'); assert.strictEqual(this.player.ads.state, 'content-playback'); }); QUnit.test('adsready in content-playback triggers readyforpreroll', function (assert) { var spy; spy = sinon.spy(); this.player.on('readyforpreroll', spy); assert.strictEqual(this.player.ads.state, 'content-set'); this.player.trigger('play'); assert.strictEqual(this.player.ads.state, 'ads-ready?'); this.player.trigger('adtimeout'); assert.strictEqual(this.player.ads.state, 'content-playback'); this.player.trigger('adsready'); assert.strictEqual(spy.getCall(0).args[0].type, 'readyforpreroll', 'readyforpreroll should have been triggered.'); }); // ---------------------------------- // Event prefixing during ad playback // ---------------------------------- QUnit.test('player events during prerolls are prefixed if tech is reused for ad', function (assert) { var prefixed, unprefixed; assert.expect(2); prefixed = sinon.spy(); unprefixed = sinon.spy(); // play a preroll this.player.on('readyforpreroll', function () { this.ads.startLinearAdMode(); }); this.player.trigger('play'); this.player.trigger('adsready'); this.player.ads.snapshot = { currentSrc: 'something' }; // simulate video events that should be prefixed this.player.on(['loadstart', 'playing', 'pause', 'ended', 'firstplay', 'loadedalldata'], unprefixed); this.player.on(['adloadstart', 'adpause', 'adended', 'adfirstplay', 'adloadedalldata'], prefixed); this.player.trigger('firstplay'); this.player.trigger('loadstart'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); this.player.trigger('pause'); this.player.trigger('ended'); assert.strictEqual(unprefixed.callCount, 0, 'no unprefixed events fired'); assert.strictEqual(prefixed.callCount, 5, 'prefixed events fired'); }); QUnit.test('player events during midrolls are prefixed if tech is reused for ad', function (assert) { var prefixed, unprefixed; assert.expect(2); prefixed = sinon.spy(); unprefixed = sinon.spy(); // play a midroll this.player.trigger('play'); this.player.trigger('adsready'); this.player.trigger('adtimeout'); this.player.ads.startLinearAdMode(); this.player.ads.snapshot = { currentSrc: 'something' }; // simulate video events that should be prefixed this.player.on(['loadstart', 'playing', 'pause', 'ended', 'firstplay', 'loadedalldata'], unprefixed); this.player.on(['adloadstart', 'adpause', 'adended', 'adfirstplay', 'adloadedalldata'], prefixed); this.player.trigger('firstplay'); this.player.trigger('loadstart'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); this.player.trigger('pause'); this.player.trigger('ended'); assert.strictEqual(unprefixed.callCount, 0, 'no unprefixed events fired'); assert.strictEqual(prefixed.callCount, 5, 'prefixed events fired'); }); QUnit.test('player events during postrolls are prefixed if tech is reused for ad', function (assert) { var prefixed, unprefixed; assert.expect(2); prefixed = sinon.spy(); unprefixed = sinon.spy(); // play a postroll this.player.trigger('play'); this.player.trigger('adsready'); this.player.trigger('adtimeout'); this.player.trigger('ended'); this.player.ads.startLinearAdMode(); this.player.ads.snapshot = { currentSrc: 'something' }; // simulate video events that should be prefixed this.player.on(['loadstart', 'playing', 'pause', 'ended', 'firstplay', 'loadedalldata'], unprefixed); this.player.on(['adloadstart', 'adpause', 'adended', 'adfirstplay', 'adloadedalldata'], prefixed); this.player.trigger('firstplay'); this.player.trigger('loadstart'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); this.player.trigger('pause'); this.player.trigger('ended'); assert.strictEqual(unprefixed.callCount, 0, 'no unprefixed events fired'); assert.strictEqual(prefixed.callCount, 5, 'prefixed events fired'); }); QUnit.test('player events during stitched ads are prefixed', function (assert) { var prefixed, unprefixed; assert.expect(2); prefixed = sinon.spy(); unprefixed = sinon.spy(); this.player.ads.stitchedAds(true); // play a midroll this.player.trigger('play'); this.player.trigger('adsready'); this.player.trigger('adtimeout'); this.player.ads.startLinearAdMode(); // simulate video events that should be prefixed this.player.on(['loadstart', 'playing', 'pause', 'ended', 'firstplay', 'loadedalldata'], unprefixed); this.player.on(['adloadstart', 'adplaying', 'adpause', 'adended', 'adfirstplay', 'adloadedalldata'], prefixed); this.player.trigger('firstplay'); this.player.trigger('loadstart'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); this.player.trigger('pause'); this.player.trigger('ended'); assert.strictEqual(unprefixed.callCount, 0, 'no unprefixed events fired'); assert.strictEqual(prefixed.callCount, 6, 'prefixed events fired'); }); QUnit.test('player events during content playback are not prefixed', function (assert) { var prefixed, unprefixed; assert.expect(3); prefixed = sinon.spy(); unprefixed = sinon.spy(); // play content this.player.trigger('play'); this.player.trigger('adsready'); this.player.trigger('adtimeout'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); // simulate video events that should not be prefixed this.player.on(['seeked', 'playing', 'pause', 'ended', 'firstplay', 'loadedalldata'], unprefixed); this.player.on(['adseeked', 'adplaying', 'adpause', 'adended', 'contentended', 'adfirstplay', 'adloadedalldata'], prefixed); this.player.trigger('firstplay'); this.player.trigger('seeked'); this.player.trigger('playing'); this.player.trigger('loadedalldata'); this.player.trigger('pause'); this.player.trigger('ended'); assert.strictEqual(unprefixed.callCount, 5, 'unprefixed events fired'); assert.strictEqual(prefixed.callCount, 1, 'prefixed events fired'); assert.strictEqual(prefixed.getCall(0).args[0].type, 'contentended', 'prefixed the ended event'); }); QUnit.test('startLinearAdMode should only trigger adstart from correct states', function (assert) { var adstart = sinon.spy(); this.player.on('adstart', adstart); this.player.ads.state = 'preroll?'; this.player.ads.startLinearAdMode(); assert.strictEqual(adstart.callCount, 1, 'preroll? state'); this.player.ads.state = 'content-playback'; this.player.ads.startLinearAdMode(); assert.strictEqual(adstart.callCount, 2, 'content-playback state'); this.player.ads.state = 'postroll?'; this.player.ads.startLinearAdMode(); assert.strictEqual(adstart.callCount, 3, 'postroll? state'); this.player.ads.state = 'content-set'; this.player.ads.startLinearAdMode(); this.player.ads.state = 'ads-ready?'; this.player.ads.startLinearAdMode(); this.player.ads.state = 'ads-ready'; this.player.ads.startLinearAdMode(); this.player.ads.state = 'ad-playback'; this.player.ads.startLinearAdMode(); assert.strictEqual(adstart.callCount, 3, 'other states'); }); QUnit.test('ad impl can notify contrib-ads there is no preroll', function (assert) { this.player.ads.state = 'preroll?'; this.player.trigger('nopreroll'); assert.strictEqual(this.player.ads.state, 'content-playback', 'no longer in preroll?'); }); QUnit.test('ad impl can notify contrib-ads there is no postroll', function (assert) { this.player.trigger('nopostroll'); this.player.ads.state = 'content-playback'; this.player.trigger('contentended'); this.clock.tick(5); assert.strictEqual(this.player.ads.state, 'content-resuming', 'no longer in postroll?'); }); QUnit.test('ended event is sent with postroll', function (assert) { var ended = sinon.spy(); this.player.tech_.el_ = { ended: true, hasChildNodes: function hasChildNodes() { return false; }, removeAttribute: function removeAttribute() {} }; this.player.on('ended', ended); this.player.ads.state = 'content-playback'; this.player.trigger('contentended'); this.clock.tick(10000); assert.ok(ended.calledOnce, 'Ended triggered'); }); QUnit.test('ended event is sent without postroll', function (assert) { var ended = sinon.spy(); this.player.tech_.el_ = { ended: true, hasChildNodes: function hasChildNodes() { return false; }, removeAttribute: function removeAttribute() {} }; this.player.on('ended', ended); this.player.ads.state = 'content-playback'; this.player.trigger('contentended'); this.clock.tick(10000); assert.ok(ended.calledOnce, 'Ended triggered'); }); QUnit.test('isLive', function (assert) { this.player.duration = function () { return 0; }; videojs.browser.IOS_VERSION = '8'; assert.strictEqual(this.player.ads.isLive(this.player), true); this.pla