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
JavaScript
(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