UNPKG

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
(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, '