videojs-contrib-ads
Version:
A framework that provides common functionality needed by video advertisement libraries working with video.js.
1,413 lines (1,102 loc) • 67.3 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){
'use strict';
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 }; }
var snapshot = {};
/**
* 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.
*/
snapshot.getPlayerSnapshot = function (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 tracks = player.remoteTextTracks ? player.remoteTextTracks() : [];
var suppressedTracks = [];
var snapshot = {
ended: player.ended(),
currentSrc: player.currentSrc(),
src: player.src(),
currentTime: currentTime,
type: player.currentType()
};
if (tech) {
snapshot.nativePoster = tech.poster;
snapshot.style = tech.getAttribute('style');
}
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
suppressedTracks.push({
track: track,
mode: track.mode
});
track.mode = 'disabled';
}
snapshot.suppressedTracks = suppressedTracks;
return snapshot;
};
/**
* Attempts to modify the specified player so that its state is equivalent to
* the state of the snapshot.
* @param {object} snapshot - the player state to apply
*/
snapshot.restorePlayerSnapshot = function (player, snapshot) {
if (player.ads.disableNextSnapshotRestore === true) {
player.ads.disableNextSnapshotRestore = false;
return;
}
// The playback tech
var tech = player.$('.vjs-tech');
// the number of[ remaining attempts to restore the snapshot
var attempts = 20;
var suppressedTracks = snapshot.suppressedTracks;
var trackSnapshot = void 0;
var restoreTracks = function restoreTracks() {
for (var i = 0; i < suppressedTracks.length; i++) {
trackSnapshot = suppressedTracks[i];
trackSnapshot.track.mode = trackSnapshot.mode;
}
};
// finish restoring the playback state
var resume = function resume() {
var currentTime = void 0;
if (_video2.default.browser.IS_IOS && player.ads.isLive(player)) {
if (snapshot.currentTime < 0) {
// Playback was behind real time, so seek backwards to match
if (player.seekable().length > 0) {
currentTime = player.seekable().end(0) + snapshot.currentTime;
} else {
currentTime = player.currentTime();
}
player.currentTime(currentTime);
}
} else {
player.currentTime(snapshot.ended ? player.duration() : snapshot.currentTime);
}
// Resume playback if this wasn't a postroll
if (!snapshot.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--) {
window.setTimeout(tryToResume, 50);
} else {
try {
resume();
} catch (e) {
_video2.default.log.warn('Failed to resume the content after an advertisement', e);
}
}
};
if (snapshot.nativePoster) {
tech.poster = snapshot.nativePoster;
}
if ('style' in snapshot) {
// overwrite all css style properties to restore state precisely
tech.setAttribute('style', snapshot.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: snapshot.currentSrc, type: snapshot.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() || !snapshot.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();
}
};
module.exports = snapshot;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],2:[function(require,module,exports){
'use strict';
QUnit.module('Ad Macros', window.sharedModuleHooks({}));
QUnit.test('player.id', function (assert) {
this.player.options_['data-player'] = '12345';
var result = this.player.ads.adMacroReplacement('{player.id}');
assert.equal(result, '12345');
});
QUnit.test('mediainfo', function (assert) {
this.player.mediainfo = {
id: 1,
name: 2,
description: 3,
tags: 4,
reference_id: 5,
duration: 6,
ad_keys: 7
};
var result = this.player.ads.adMacroReplacement('{mediainfo.id}' + '{mediainfo.name}' + '{mediainfo.description}' + '{mediainfo.tags}' + '{mediainfo.reference_id}' + '{mediainfo.duration}' + '{mediainfo.ad_keys}');
assert.equal(result, '1234567');
});
QUnit.test('player.duration', function (assert) {
this.player.duration = function () {
return 5;
};
var result = this.player.ads.adMacroReplacement('{player.duration}');
assert.equal(result, 5);
});
QUnit.test('timestamp', function (assert) {
this.player.duration = function () {
return 5;
};
var result = this.player.ads.adMacroReplacement('{timestamp}');
assert.equal(result, new Date().getTime());
});
QUnit.test('document.referrer', function (assert) {
var result = this.player.ads.adMacroReplacement('{document.referrer}');
assert.ok(result.match(/http:\/\/localhost:\d+\/\?id=\d+/), '"' + result + '" was the document.referrer');
});
QUnit.test('window.location.href', function (assert) {
var result = this.player.ads.adMacroReplacement('{window.location.href}');
assert.ok(result.match(/http:\/\/localhost:\d+\/context.html/), '"' + result + '" was the window.location.href');
});
QUnit.test('random', function (assert) {
var result = this.player.ads.adMacroReplacement('{random}');
assert.ok(result.match(/^\d+$/), '"' + result + '" is a random number');
});
QUnit.test('mediainfo.custom_fields', function (assert) {
this.player.mediainfo = {
custom_fields: {
dog: 1,
cat: 2,
guinea_pig: 3
},
customFields: {
dog: 1,
cat: 2,
guinea_pig: 3
}
};
var result = this.player.ads.adMacroReplacement('{mediainfo.custom_fields.dog}' + '{mediainfo.custom_fields.cat}' + '{mediainfo.custom_fields.guinea_pig}' + '{mediainfo.customFields.dog}' + '{mediainfo.customFields.cat}' + '{mediainfo.customFields.guinea_pig}');
assert.equal(result, '123123');
});
QUnit.test('pageVariables', function (assert) {
window.animal = {
dog: 'Old Buddy',
cat: {
maineCoon: 'Huge the Cat',
champion: {
name: 'Champ'
}
}
};
window.bird = null;
window.isAwesome = true;
window.foo = function () {};
window.bar = {};
var result = this.player.ads.adMacroReplacement('Number: {pageVariable.scrollX}, ' + 'Boolean: {pageVariable.isAwesome}, ' + 'Null: {pageVariable.bird}, ' + 'Undefined: {pageVariable.thisDoesNotExist}, ' + 'Function: {pageVariable.foo}, ' + 'Object: {pageVariable.bar}, ' + 'Nested 2x: {pageVariable.animal.dog}, ' + 'Nested 3x: {pageVariable.animal.cat.maineCoon}, ' + 'Nested 4x: {pageVariable.animal.cat.champion.name}');
assert.equal(result, 'Number: 0, ' + 'Boolean: true, ' + 'Null: null, ' + 'Undefined: , ' + 'Function: , ' + 'Object: , ' + 'Nested 2x: Old Buddy, ' + 'Nested 3x: Huge the Cat, ' + 'Nested 4x: Champ');
});
QUnit.test('uriEncode', function (assert) {
this.player.mediainfo = {
custom_fields: {
urlParam: '? &'
}
};
window.foo = '& ?';
var result = this.player.ads.adMacroReplacement('{mediainfo.custom_fields.urlParam}{pageVariable.foo}', true);
assert.equal(result, '%3F%20%26%26%20%3F');
});
QUnit.test('customMacros', function (assert) {
var result = this.player.ads.adMacroReplacement('The sky is {skyColor}. {exclamation}!', false, {
'{skyColor}': 'blue',
'{exclamation}': 'Hooray'
});
assert.equal(result, 'The sky is blue. Hooray!');
});
},{}],3:[function(require,module,exports){
'use strict';
var _snapshot = require('../src/snapshot.js');
var _snapshot2 = _interopRequireDefault(_snapshot);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
QUnit.module('Video Snapshot', window.sharedModuleHooks({
beforeEach: function beforeEach() {
var captionTrack = document.createElement('track');
var otherTrack = document.createElement('track');
captionTrack.setAttribute('kind', 'captions');
captionTrack.setAttribute('src', 'testcaption.vtt');
otherTrack.setAttribute('src', 'testcaption.vtt');
this.video.appendChild(captionTrack);
this.video.appendChild(otherTrack);
this.player.ended = function () {
return false;
};
}
}));
QUnit.test('restores the original video src after ads', function (assert) {
var originalSrc;
assert.expect(1);
originalSrc = this.player.currentSrc();
this.player.trigger('adsready');
this.player.trigger('play');
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
this.player.ads.endLinearAdMode();
assert.strictEqual(this.player.currentSrc(), originalSrc, 'the original src is restored');
});
QUnit.test('waits for the video to become seekable before restoring the time', function (assert) {
var setTimeoutSpy;
assert.expect(2);
this.player.trigger('adsready');
this.player.trigger('play');
// the video plays to time 100
setTimeoutSpy = sinon.spy(window, 'setTimeout');
this.video.currentTime = 100;
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
// the ad resets the current time
this.video.currentTime = 0;
this.player.ads.endLinearAdMode();
setTimeoutSpy.reset(); // we call setTimeout an extra time restorePlayerSnapshot
this.player.trigger('canplay');
assert.strictEqual(setTimeoutSpy.callCount, 1, 'restoring the time should be delayed');
assert.strictEqual(this.video.currentTime, 0, 'currentTime is not modified');
window.setTimeout.restore();
});
QUnit.test('tries to restore the play state up to 20 times', function (assert) {
var setTimeoutSpy;
assert.expect(1);
this.player.trigger('adsready');
this.player.trigger('play');
// the video plays to time 100
this.video.currentTime = 100;
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
setTimeoutSpy = sinon.spy(window, 'setTimeout');
// the ad resets the current time
this.video.currentTime = 0;
this.player.ads.endLinearAdMode();
setTimeoutSpy.reset(); // we call setTimeout an extra time restorePlayerSnapshot
this.player.trigger('canplay');
// We expect 20 timeouts at 50ms each.
this.clock.tick(1000);
assert.strictEqual(setTimeoutSpy.callCount, 20, 'seekable was tried multiple times');
window.setTimeout.restore();
});
QUnit.test('the current time is restored at the end of an ad', function (assert) {
assert.expect(1);
this.player.trigger('adsready');
this.video.currentTime = 100;
this.player.trigger('play');
// the video plays to time 100
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
// the ad resets the current time
this.video.currentTime = 0;
this.player.ads.endLinearAdMode();
this.player.trigger('canplay');
this.clock.tick(1000);
assert.strictEqual(this.video.currentTime, 100, 'currentTime was restored');
});
QUnit.test('only restores the player snapshot if the src changed', function (assert) {
var playSpy, srcSpy, currentTimeSpy;
this.player.trigger('adsready');
this.player.trigger('play');
playSpy = sinon.spy(this.player, 'play');
srcSpy = sinon.spy(this.player, 'src');
currentTimeSpy = sinon.spy(this.player, 'currentTime');
// with a separate video display or server-side ad insertion, ads play but
// the src never changes. Modifying the src or currentTime would introduce
// unnecessary seeking and rebuffering
this.player.ads.startLinearAdMode();
this.player.ads.endLinearAdMode();
assert.ok(playSpy.called, 'content playback resumed');
assert.ok(srcSpy.alwaysCalledWithExactly(), 'the src was not reset');
this.player.trigger('playing');
// the src wasn't changed, so we shouldn't be waiting on loadedmetadata to
// update the currentTime
this.player.trigger('loadedmetadata');
assert.ok(currentTimeSpy.alwaysCalledWithExactly(), 'no seeking occurred');
});
QUnit.test('snapshot does not resume playback after post-rolls', function (assert) {
var playSpy = sinon.spy(this.player, 'play');
// start playback
this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4');
this.player.trigger('loadstart');
this.player.trigger('loadedmetadata');
this.player.trigger('adsready');
this.player.tech_.trigger('play');
// trigger an ad
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
this.player.trigger('loadstart');
this.player.trigger('loadedmetadata');
this.player.ads.endLinearAdMode();
// resume playback
this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4');
this.player.trigger('loadstart');
this.player.trigger('canplay');
// "canplay" causes the `restorePlayerSnapshot` function in the plugin
// to be called. This causes content playback to be resumed after 20
// attempts of a 50ms timeout (20 * 50 == 1000).
this.clock.tick(1000);
assert.strictEqual(playSpy.callCount, 1, 'content playback resumed');
this.player.trigger('playing');
// if the video ends (regardless of burned in post-roll or otherwise) when
// stopLinearAdMode fires next we should not hit play() since we have reached
// the end of the stream
this.player.ended = function () {
return true;
};
this.player.trigger('ended');
playSpy.reset();
// trigger a post-roll
this.player.ads.startLinearAdMode();
this.player.src('//example.com/ad.mp4');
this.player.trigger('loadstart');
this.player.trigger('loadedmetadata');
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
this.player.trigger('ended');
assert.strictEqual(this.player.ads.state, 'content-playback', 'Player should be in content-playback state after a post-roll');
assert.strictEqual(playSpy.callCount, 0, 'content playback should not have been resumed');
});
QUnit.test('snapshot does not resume playback after a burned-in post-roll', function (assert) {
var playSpy, loadSpy;
this.player.trigger('adsready');
this.player.trigger('play');
playSpy = sinon.spy(this.player, 'play');
loadSpy = sinon.spy(this.player, 'load');
this.player.ads.startLinearAdMode();
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
assert.ok(playSpy.called, 'content playback resumed');
// if the video ends (regardless of burned in post-roll or otherwise) when
// stopLinearAdMode fires next we should not hit play() since we have reached
// the end of the stream
this.player.ended = function () {
return true;
};
this.player.trigger('ended');
playSpy.reset();
// trigger a post-roll
this.player.currentTime(30);
this.player.ads.startLinearAdMode();
this.player.currentTime(50);
this.player.ads.endLinearAdMode();
this.player.trigger('ended');
assert.strictEqual(this.player.ads.state, 'content-playback', 'Player should be in content-playback state after a post-roll');
assert.strictEqual(this.player.currentTime(), 50, 'currentTime should not be reset using burned in ads');
assert.notOk(loadSpy.called, 'player.load() should not be called if the player is ended.');
assert.notOk(playSpy.called, 'content playback should not have been resumed');
});
QUnit.test('snapshot does not resume playback after multiple post-rolls', function (assert) {
var playSpy;
this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4');
this.player.trigger('loadstart');
this.player.trigger('adsready');
this.player.trigger('play');
playSpy = sinon.spy(this.player, 'play');
// with a separate video display or server-side ad insertion, ads play but
// the src never changes. Modifying the src or currentTime would introduce
// unnecessary seeking and rebuffering
this.player.ads.startLinearAdMode();
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
assert.ok(playSpy.called, 'content playback resumed');
// if the video ends (regardless of burned in post-roll or otherwise) when
// stopLinearAdMode fires next we should not hit play() since we have reached
// the end of the stream
this.player.ended = function () {
return true;
};
this.player.trigger('ended');
playSpy.reset();
// trigger a lot of post-rolls
this.player.ads.startLinearAdMode();
this.player.src('http://example.com/ad1.mp4');
this.player.trigger('loadstart');
this.player.src('http://example.com/ad2.mp4');
this.player.trigger('loadstart');
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
this.player.trigger('ended');
assert.strictEqual(this.player.ads.state, 'content-playback', 'Player should be in content-playback state after a post-roll');
assert.notOk(playSpy.called, 'content playback should not resume');
});
QUnit.test('changing the source and then timing out does not restore a snapshot', function (assert) {
this.player.paused = function () {
return false;
};
// load and play the initial video
this.player.src('http://example.com/movie.mp4');
this.player.trigger('loadstart');
this.player.trigger('play');
this.player.trigger('adsready');
// preroll
this.player.ads.startLinearAdMode();
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
// change the content and timeout the new ad response
this.player.src('http://example.com/movie2.mp4');
this.player.trigger('loadstart');
this.player.trigger('adtimeout');
assert.strictEqual(this.player.ads.state, 'content-playback', 'playing the new content video after the ad timeout');
assert.strictEqual('http://example.com/movie2.mp4', this.player.currentSrc(), 'playing the second video');
});
// changing the src attribute to a URL that AdBlocker is intercepting
// doesn't update currentSrc, so when restoring the snapshot we
// should check for src attribute modifications as well
QUnit.test('checks for a src attribute change that isn\'t reflected in currentSrc', function (assert) {
var updatedSrc;
this.player.currentSrc = function () {
return 'content.mp4';
};
this.player.currentType = function () {
return 'video/mp4';
};
this.player.trigger('adsready');
this.player.trigger('play');
this.player.ads.startLinearAdMode();
// `src` gets called internally to set the source back to its original
// value when the player snapshot is restored when `endLinearAdMode`
// is called.
this.player.src = function (source) {
if (source === undefined) {
return 'ad.mp4';
}
updatedSrc = source;
};
this.player.ads.endLinearAdMode();
this.player.trigger('playing');
assert.deepEqual(updatedSrc, { src: 'content.mp4', type: 'video/mp4' }, 'restored src attribute');
});
QUnit.test('When captions are enabled, the video\'s tracks will be disabled during the ad', function (assert) {
var tracks = this.player.remoteTextTracks ? this.player.remoteTextTracks() : [];
var showing = 0;
var disabled = 0;
var i;
if (tracks.length <= 0) {
assert.expect(0);
videojs.log.warn('Did not detect text track support, skipping');
return;
}
assert.expect(3);
this.player.trigger('adsready');
this.player.trigger('play');
// set all modes to 'showing'
for (i = 0; i < tracks.length; i++) {
tracks[i].mode = 'showing';
}
for (i = 0; i < tracks.length; i++) {
if (tracks[i].mode === 'showing') {
showing++;
}
}
assert.strictEqual(showing, tracks.length, 'all tracks should be showing');
showing = 0;
this.player.ads.startLinearAdMode();
for (i = 0; i < tracks.length; i++) {
if (tracks[i].mode === 'disabled') {
disabled++;
}
}
assert.strictEqual(disabled, tracks.length, 'all tracks should be disabled');
this.player.ads.endLinearAdMode();
for (i = 0; i < tracks.length; i++) {
if (tracks[i].mode === 'showing') {
showing++;
}
}
assert.strictEqual(showing, tracks.length, 'all tracks should be showing');
});
QUnit.test('player events during snapshot restoration are prefixed', function (assert) {
var spy = sinon.spy();
assert.expect(2);
this.player.on(['contentloadstart', 'contentloadedmetadata'], spy);
this.player.src({
src: 'http://example.com/movie.mp4',
type: 'video/mp4'
});
this.player.on('readyforpreroll', function () {
this.ads.startLinearAdMode();
});
this.player.trigger('adsready');
this.player.trigger('play');
// change the source to an ad
this.player.src({
src: 'http://example.com/ad.mp4',
type: 'video/mp4'
});
this.player.trigger('loadstart');
assert.strictEqual(spy.callCount, 0, 'did not fire contentloadstart');
this.player.ads.endLinearAdMode();
// make it appear that the tech is ready to seek
this.player.trigger('loadstart');
this.player.trigger('loadedmetadata');
assert.strictEqual(spy.callCount, 2, 'fired "content" prefixed events');
});
QUnit.test('No snapshot if duration is Infinity', function (assert) {
var originalSrc = 'foobar';
var newSrc = 'barbaz';
this.player.duration(Infinity);
this.player.src(originalSrc);
this.player.trigger('adsready');
this.player.trigger('play');
this.player.ads.startLinearAdMode();
this.player.src(newSrc);
this.player.ads.endLinearAdMode();
assert.strictEqual(this.player.currentSrc(), newSrc, 'source is not reset');
});
QUnit.test('Snapshot and text tracks', function (assert) {
var trackSrc = 'http://solutions.brightcove.com/' + 'bcls/captions/adding_captions_to_videos_french.vtt';
// No text tracks at start
assert.equal(this.player.remoteTextTracks().length, 0);
// Add a text track
this.player.addRemoteTextTrack({
kind: 'captions',
language: 'fr',
label: 'French',
src: trackSrc
});
// Show our new text track, since it's disabled by default
this.player.remoteTextTracks()[0].mode = 'showing';
// Text track looks good
assert.equal(this.player.remoteTextTracks().length, 1);
assert.equal(this.player.remoteTextTrackEls().trackElements_[0].src, trackSrc);
assert.equal(this.player.remoteTextTracks()[0].kind, 'captions');
assert.equal(this.player.remoteTextTracks()[0].language, 'fr');
assert.equal(this.player.remoteTextTracks()[0].mode, 'showing');
// Do a snapshot, as if an ad is starting
this.player.ads.snapshot = _snapshot2.default.getPlayerSnapshot(this.player);
// Snapshot reflects the text track
assert.equal(this.player.ads.snapshot.suppressedTracks.length, 1);
assert.equal(this.player.ads.snapshot.suppressedTracks[0].track.kind, 'captions');
assert.equal(this.player.ads.snapshot.suppressedTracks[0].track.language, 'fr');
assert.equal(this.player.ads.snapshot.suppressedTracks[0].mode, 'showing');
// Meanwhile, track is intact, just disabled
assert.equal(this.player.remoteTextTracks().length, 1);
assert.equal(this.player.remoteTextTrackEls().trackElements_[0].src, trackSrc);
assert.equal(this.player.remoteTextTracks()[0].kind, 'captions');
assert.equal(this.player.remoteTextTracks()[0].language, 'fr');
assert.equal(this.player.remoteTextTracks()[0].mode, 'disabled');
// Restore the snapshot, as if an ad is ending
_snapshot2.default.restorePlayerSnapshot(this.player, this.player.ads.snapshot);
// Everything is back to normal
assert.equal(this.player.remoteTextTracks().length, 1);
assert.equal(this.player.remoteTextTrackEls().trackElements_[0].src, trackSrc);
assert.equal(this.player.remoteTextTracks()[0].kind, 'captions');
assert.equal(this.player.remoteTextTracks()[0].language, 'fr');
assert.equal(this.player.remoteTextTracks()[0].mode, 'showing');
});
},{"../src/snapshot.js":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, '