diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a184176c..18d9307a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,30 +1,3 @@
# CONTRIBUTING
-We welcome contributions from everyone!
-
-## Getting Started
-
-Make sure you have NodeJS 0.10 or higher and npm installed.
-
-1. Fork this repository and clone your fork
-1. Install dependencies: `npm install`
-1. Run a development server: `npm start`
-
-### Making Changes
-
-Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
-
-When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
-
-### Running Tests
-
-Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
-
-- In all available and supported browsers: `npm test`
-- In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
-- While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
-
-
-[karma]: http://karma-runner.github.io/
-[local]: http://localhost:9999/test/
-[conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md
+See: [docs/developer/index.md](docs/developer/index.md)
diff --git a/docs/developer/getting-started.md b/docs/developer/getting-started.md
index 8663bd30..bd44d9c1 100644
--- a/docs/developer/getting-started.md
+++ b/docs/developer/getting-started.md
@@ -35,7 +35,7 @@ npm run test
### In browser
-Run `./node_modules/.bin/karma start --no-single-run --browsers Chrome test/karma.conf.js` then open `localhost:9876/debug.html`. This can be useful for debugging tests.
+Run `npm start` and a Chrome instance will launch with Karma's debug interface at `localhost:9876`, allowing you to debug tests. Also, a static server will run and allow you to look at examples at `localhost:9999`.
## What's Next
diff --git a/examples/stitched-ad-plugin/app.css b/examples/stitched-ad-plugin/app.css
new file mode 100644
index 00000000..3d7e9b8f
--- /dev/null
+++ b/examples/stitched-ad-plugin/app.css
@@ -0,0 +1,25 @@
+.ad-event {
+ background-color: #ffe400;
+}
+.content-event {
+ background-color: #66a8cc;
+}
+.content-adplugin-event {
+ background-color: #00a866;
+}
+ol {
+ list-style-type: none;
+ padding: 0;
+ font-family: monospace;
+ white-space: pre;
+}
+
+/* cue-text-tracks-example Styles */
+
+.ad-container .vjs-play-progress.vjs-slider-bar {
+ background-color: #ff0000;
+}
+
+.ad-container .vjs-big-play-button {
+ display: none;
+}
diff --git a/examples/stitched-ad-plugin/app.js b/examples/stitched-ad-plugin/app.js
new file mode 100644
index 00000000..fd18764a
--- /dev/null
+++ b/examples/stitched-ad-plugin/app.js
@@ -0,0 +1,99 @@
+(function() {
+ 'use strict';
+ var pad = function(n, x, c) {
+ return (new Array(n).join(c || '0') + x).slice(-n);
+ };
+ var padRight = function(n, x, c) {
+ return (x + (new Array(n).join(c || '0'))).slice(0, n);
+ };
+
+ var player = window.player = videojs('examplePlayer');
+
+ // initalize example ad plugin for this player
+ player.exampleStitchedAds({
+ debug: true
+ });
+
+ var log = document.querySelector('.log');
+ var Html5 = videojs.getTech('Html5');
+
+ var eventList = Html5.Events.concat(Html5.Events.map(function(evt) {
+ return 'ad' + evt;
+ })).concat(Html5.Events.map(function(evt) {
+ return 'content' + evt;
+ })).concat([
+ // events emitted by ad plugin
+ 'adtimeout',
+ 'contentupdate',
+ 'contentplayback',
+ 'readyforpreroll',
+ 'readyforpostroll',
+ // events emitted by third party ad implementors
+ 'adsready',
+ 'adscanceled',
+ 'adplaying',
+ 'adstart', // startLinearAdMode()
+ 'adend', // endLinearAdMode()
+ 'nopreroll',
+ 'nopostroll'
+
+ ]).filter(function(evt) {
+ var events = {
+ progress: 1,
+ timeupdate: 1,
+ suspend: 1,
+ emptied: 1,
+ durationchange: 1,
+ contentprogress: 1,
+ contenttimeupdate: 1,
+ contentsuspend: 1,
+ contentemptied: 1,
+ adprogress: 1,
+ adtimeupdate: 1,
+ adsuspend: 1,
+ ademptied: 1,
+ adabort: 1
+ }
+
+ return !(evt in events);
+ });
+
+ player.on(eventList, function(event) {
+ var d , str, li, evt;
+
+ evt = event.type;
+ li = document.createElement('li');
+
+ d = new Date();
+ d = '' +
+ pad(2, d.getHours()) + ':' +
+ pad(2, d.getMinutes()) + ':' +
+ pad(2, d.getSeconds()) + '.' +
+ pad(3, d.getMilliseconds());
+
+ if (event.type.indexOf('ad') === 0) {
+ li.className = 'ad-event';
+ } else if (event.type.indexOf('content') === 0) {
+ li.className = 'content-event';
+ }
+
+ str = d + ' ' + evt;
+
+ if (evt === 'contentupdate') {
+ str += ' ' + event.oldValue + " -> " + event.newValue;
+ li.className = 'content-adplugin-event';
+ }
+ if (evt === 'contentchanged') {
+ li.className = 'content-adplugin-event';
+ }
+ if (evt === 'contentplayback') {
+ li.className = 'content-adplugin-event';
+ }
+ if (evt === 'adplay') {
+ player.trigger('ads-ad-started');
+ }
+
+ li.innerHTML = str;
+ log.insertBefore(li, log.firstChild);
+ });
+})();
diff --git a/examples/stitched-ad-plugin/index.html b/examples/stitched-ad-plugin/index.html
new file mode 100644
index 00000000..e0609729
--- /dev/null
+++ b/examples/stitched-ad-plugin/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+ Stitched Ads Example
+
+
+
+
+
+
+
+ Stitched Ads Example
+
+ This page uses the built files, so you must do a local build before the example will work.
+
+ This is a limited example implementation that puts the player into and out of ad mode for 5 seconds at specific times.
+
+
+
+
+
+
+
+ Events
+
+
+
+
+
+
+
diff --git a/examples/stitched-ad-plugin/plugin.js b/examples/stitched-ad-plugin/plugin.js
new file mode 100644
index 00000000..26f73e0e
--- /dev/null
+++ b/examples/stitched-ad-plugin/plugin.js
@@ -0,0 +1,68 @@
+
+// This is an example of a very simple, naive stitched ads plugin. This means
+// there is a single source within which are the "ads".
+//
+// We'll simulate a preroll for the first 5 seconds of playback, a midroll at
+// 15 seconds, and a postroll.
+//
+// In reality, it's left up to the integration to define when and where ad
+// mode is started and ended via some kind of mapping or manifest.
+//
+videojs.registerPlugin('exampleStitchedAds', function(options) {
+ var player = this;
+
+ options = options || {};
+ options.stitchedAds = true;
+
+ // Initialize contrib-ads.
+ player.ads(options);
+
+ player.on('canplay', function() {
+ player.one('playing', function() {
+ var havePlayedPreroll = false;
+ var haveStartedMidroll = false;
+ var haveStartedPostroll = false;
+ var havePlayedMidroll = false;
+
+ // Simulate a pre-roll immediately upon playback starting.
+ player.ads.startLinearAdMode();
+
+ // Simulate a mid-roll at 15 seconds and a post-roll at 5 seconds from
+ // the end. Need to listen to both timeupdate and adtimeupdate because
+ // redispatch will prefix during ads.
+ player.on(['timeupdate', 'adtimeupdate'], function(e) {
+ var currentTime = player.currentTime();
+ var duration = player.duration();
+
+ // End the pre-roll.
+ if (!havePlayedPreroll && currentTime >= 5) {
+ havePlayedPreroll = true;
+ player.ads.endLinearAdMode();
+ return;
+ }
+
+ // Start the mid-roll.
+ if (!haveStartedMidroll && currentTime >= 15) {
+ haveStartedMidroll = true;
+ player.ads.startLinearAdMode();
+ return;
+ }
+
+ // End the mid-roll.
+ if (!havePlayedMidroll && currentTime >= 20) {
+ havePlayedMidroll = true;
+ player.ads.endLinearAdMode();
+ return;
+ }
+
+ // Start the post-roll.
+ // The post-roll will be ended automatically via the `adended` event.
+ if (!haveStartedPostroll && currentTime >= duration - 5) {
+ haveStartedPostroll = true;
+ player.ads.startLinearAdMode();
+ return;
+ }
+ });
+ });
+ });
+});
diff --git a/index.html b/index.html
index 3d613940..83a305f4 100644
--- a/index.html
+++ b/index.html
@@ -15,6 +15,7 @@
Example ad plugin.
Example cueTextTracks plugin.
Example using modules
+ Example stitched ad plugin.
diff --git a/package.json b/package.json
index f22a29eb..034e260d 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,9 @@
"postclean": "mkdirp dist test/dist",
"lint": "eslint src",
"prestart": "npm run build",
- "start": "npm-run-all -p start:server watch",
+ "start": "npm-run-all -p start:* watch",
"start:server": "static -a 0.0.0.0 -p 9999 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}' .",
+ "start:test": "karma start --no-single-run --browsers Chrome test/karma.conf.js",
"pretest": "npm-run-all lint build",
"test": "karma start test/karma.conf.js",
"preversion": "npm test",
diff --git a/scripts/modules.rollup.config.js b/scripts/modules.rollup.config.js
index 682aacea..1b69f3c9 100644
--- a/scripts/modules.rollup.config.js
+++ b/scripts/modules.rollup.config.js
@@ -9,8 +9,8 @@ import babel from 'rollup-plugin-babel';
import json from 'rollup-plugin-json';
export default {
- moduleName: 'videojsContribAds',
- entry: 'src/plugin.js',
+ name: 'videojsContribAds',
+ input: 'src/plugin.js',
external: [
'global/document',
'global/window',
@@ -19,6 +19,13 @@ export default {
globals: {
'video.js': 'videojs'
},
+ output: [{
+ file: 'dist/videojs-contrib-ads.cjs.js',
+ format: 'cjs',
+ }, {
+ file: 'dist/videojs-contrib-ads.es.js',
+ format: 'es'
+ }],
legacy: true,
plugins: [
json(),
@@ -37,9 +44,5 @@ export default {
'transform-object-assign'
]
})
- ],
- targets: [
- {dest: 'dist/videojs-contrib-ads.cjs.js', format: 'cjs'},
- {dest: 'dist/videojs-contrib-ads.es.js', format: 'es'}
]
};
diff --git a/scripts/test.rollup.config.js b/scripts/test.rollup.config.js
index 1d0c2e71..a7cae9a3 100644
--- a/scripts/test.rollup.config.js
+++ b/scripts/test.rollup.config.js
@@ -10,10 +10,12 @@ import multiEntry from 'rollup-plugin-multi-entry';
import resolve from 'rollup-plugin-node-resolve';
export default {
- moduleName: 'videojsContribAdsTests',
- entry: 'test/**/test.*.js',
- dest: 'test/dist/bundle.js',
- format: 'iife',
+ name: 'videojsContribAdsTests',
+ input: 'test/**/test.*.js',
+ output: {
+ file: 'test/dist/bundle.js',
+ format: 'iife'
+ },
external: [
'qunit',
'qunitjs',
diff --git a/scripts/umd.rollup.config.js b/scripts/umd.rollup.config.js
index 8fb344c0..d8ffc35f 100644
--- a/scripts/umd.rollup.config.js
+++ b/scripts/umd.rollup.config.js
@@ -10,10 +10,12 @@ import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
export default {
- moduleName: 'videojsContribAds',
- entry: 'src/plugin.js',
- dest: 'dist/videojs-contrib-ads.js',
- format: 'umd',
+ name: 'videojsContribAds',
+ input: 'src/plugin.js',
+ output: {
+ file: 'dist/videojs-contrib-ads.js',
+ format: 'umd'
+ },
external: ['video.js'],
globals: {
'video.js': 'videojs'
diff --git a/src/adBreak.js b/src/adBreak.js
index dead52c7..b67cf0f8 100644
--- a/src/adBreak.js
+++ b/src/adBreak.js
@@ -15,7 +15,7 @@ function start(player) {
player.trigger('adstart');
// Capture current player state snapshot
- if (!player.ads.shouldPlayContentBehindAd(player)) {
+ if (player.ads.shouldTakeSnapshots()) {
player.ads.snapshot = snapshot.getPlayerSnapshot(player);
}
@@ -64,7 +64,7 @@ function end(player, callback) {
}
// Restore snapshot
- if (!player.ads.shouldPlayContentBehindAd(player)) {
+ if (player.ads.shouldTakeSnapshots()) {
snapshot.restorePlayerSnapshot(player, callback);
// Reset the volume to pre-ad levels
diff --git a/src/ads.js b/src/ads.js
index 41ba2be0..ba2ffbd2 100644
--- a/src/ads.js
+++ b/src/ads.js
@@ -86,11 +86,20 @@ export default function getAds(player) {
player.ads._state.skipLinearAdMode();
},
+ // With no arguments, returns a boolean value indicating whether or not
+ // contrib-ads is set to treat ads as stitched with content in a single
+ // stream. With arguments, treated as a setter, but this behavior is
+ // deprecated.
stitchedAds(arg) {
if (arg !== undefined) {
- this._stitchedAds = !!arg;
+ videojs.log.warn('Using player.ads.stitchedAds() as a setter is deprecated, it should be set as an option upon initialization of contrib-ads.');
+
+ // Keep the private property and the settings in sync. When this
+ // setter is removed, we can probably stop using the private property.
+ this.settings.stitchedAds = !!arg;
}
- return this._stitchedAds;
+
+ return this.settings.stitchedAds;
},
// Returns whether the video element has been modified since the
@@ -116,7 +125,7 @@ export default function getAds(player) {
// Returns a boolean indicating if given player is in live mode.
// One reason for this: https://github.com/videojs/video.js/issues/3262
// Also, some live content can have a duration.
- isLive(somePlayer) {
+ isLive(somePlayer = player) {
if (typeof somePlayer.ads.settings.contentIsLive === 'boolean') {
return somePlayer.ads.settings.contentIsLive;
} else if (somePlayer.duration() === Infinity) {
@@ -130,7 +139,7 @@ export default function getAds(player) {
// Return true if content playback should mute and continue during ad breaks.
// This is only done during live streams on platforms where it's supported.
// This improves speed and accuracy when returning from an ad break.
- shouldPlayContentBehindAd(somePlayer) {
+ shouldPlayContentBehindAd(somePlayer = player) {
if (!somePlayer) {
throw new Error('shouldPlayContentBehindAd requires a player as a param');
} else if (!somePlayer.ads.settings.liveCuePoints) {
@@ -142,6 +151,12 @@ export default function getAds(player) {
}
},
+ // Return true if the ads plugin should save and restore snapshots of the
+ // player state when moving into and out of ad mode.
+ shouldTakeSnapshots(somePlayer = player) {
+ return !this.shouldPlayContentBehindAd(somePlayer) && !this.stitchedAds();
+ },
+
// Returns true if player is in ad mode.
//
// Ad mode definition:
diff --git a/src/plugin.js b/src/plugin.js
index dd59ab3f..5c331cac 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -13,7 +13,7 @@ import cueTextTracks from './cueTextTracks.js';
import initCancelContentPlay from './cancelContentPlay.js';
import playMiddlewareFeature from './playMiddleware.js';
-import {BeforePreroll} from './states.js';
+import {BeforePreroll, StitchedContentPlayback} from './states.js';
const { playMiddleware, isMiddlewareMediatorSupported } = playMiddlewareFeature;
const VIDEO_EVENTS = videojs.getTech('Html5').Events;
@@ -177,10 +177,21 @@ const contribAdsPlugin = function(options) {
player.ads = getAds(player);
player.ads.settings = settings;
- player.ads._state = new BeforePreroll(player);
- player.ads._state.init(player);
- player.ads.stitchedAds(settings.stitchedAds);
+ // Set the stitched ads state. This needs to happen before the `_state` is
+ // initialized below - BeforePreroll needs to know whether contrib-ads is
+ // playing stitched ads or not.
+ // The setter is deprecated, so this does not use it.
+ // But first, cast to boolean.
+ settings.stitchedAds = !!settings.stitchedAds;
+
+ if (settings.stitchedAds) {
+ player.ads._state = new StitchedContentPlayback(player);
+ } else {
+ player.ads._state = new BeforePreroll(player);
+ }
+
+ player.ads._state.init(player);
player.ads.cueTextTracks = cueTextTracks;
player.ads.adMacroReplacement = adMacroReplacement.bind(player);
@@ -235,7 +246,7 @@ const contribAdsPlugin = function(options) {
// Event handling for the current state.
player.on([
'play', 'playing', 'ended',
- 'adsready', 'adscanceled', 'adskip', 'adserror', 'adtimeout',
+ 'adsready', 'adscanceled', 'adskip', 'adserror', 'adtimeout', 'adended',
'ads-ad-started',
'contentchanged', 'dispose', 'contentresumed', 'readyforpostroll',
'nopreroll', 'nopostroll'], (e) => {
diff --git a/src/redispatch.js b/src/redispatch.js
index d7f90a90..267a0948 100644
--- a/src/redispatch.js
+++ b/src/redispatch.js
@@ -86,7 +86,7 @@ const handleEnded = (player, event) => {
}
// Prefix ended due to content ending before postroll check
- } else if (!player.ads._contentHasEnded) {
+ } else if (!player.ads._contentHasEnded && !player.ads.stitchedAds()) {
// This will change to cancelEvent after the contentended deprecation
// period (contrib-ads 7)
diff --git a/src/states.js b/src/states.js
index 90fbe8f3..b68deaa9 100644
--- a/src/states.js
+++ b/src/states.js
@@ -11,6 +11,8 @@ import Postroll from './states/Postroll.js';
import BeforePreroll from './states/BeforePreroll.js';
import ContentPlayback from './states/ContentPlayback.js';
import AdsDone from './states/AdsDone.js';
+import StitchedAdRoll from './states/StitchedAdRoll.js';
+import StitchedContentPlayback from './states/StitchedContentPlayback.js';
export {
State,
@@ -21,5 +23,7 @@ export {
Postroll,
BeforePreroll,
ContentPlayback,
- AdsDone
+ AdsDone,
+ StitchedAdRoll,
+ StitchedContentPlayback
};
diff --git a/src/states/StitchedAdRoll.js b/src/states/StitchedAdRoll.js
new file mode 100644
index 00000000..e2d1659a
--- /dev/null
+++ b/src/states/StitchedAdRoll.js
@@ -0,0 +1,54 @@
+import {AdState, StitchedContentPlayback} from '../states.js';
+import adBreak from '../adBreak.js';
+
+export default class StitchedAdRoll extends AdState {
+
+ /*
+ * Allows state name to be logged even after minification.
+ */
+ static _getName() {
+ return 'StitchedAdRoll';
+ }
+
+ /*
+ * StitchedAdRoll breaks happen when the ad plugin calls startLinearAdMode,
+ * which can happen at any time during content playback.
+ */
+ init() {
+ this.waitingForAdBreak = false;
+ this.contentResuming = false;
+ this.player.ads.adType = 'stitched';
+ adBreak.start(this.player);
+ }
+
+ /*
+ * For stitched ads, there is no "content resuming" scenario, so a "playing"
+ * event is not relevant.
+ */
+ onPlaying() {}
+
+ /*
+ * For stitched ads, there is no "content resuming" scenario, so a
+ * "contentresumed" event is not relevant.
+ */
+ onContentResumed() {}
+
+ /*
+ * When we see an "adended" event, it means that we are in a postroll that
+ * has ended (because the media ended and we are still in an ad state).
+ *
+ * In these cases, we transition back to content mode and fire ended.
+ */
+ onAdEnded() {
+ this.endLinearAdMode();
+ this.player.trigger('ended');
+ }
+
+ /*
+ * StitchedAdRoll break is done.
+ */
+ endLinearAdMode() {
+ adBreak.end(this.player);
+ this.transitionTo(StitchedContentPlayback);
+ }
+}
diff --git a/src/states/StitchedContentPlayback.js b/src/states/StitchedContentPlayback.js
new file mode 100644
index 00000000..cea12f6b
--- /dev/null
+++ b/src/states/StitchedContentPlayback.js
@@ -0,0 +1,40 @@
+import {ContentState, StitchedAdRoll} from '../states.js';
+
+/*
+ * This state represents content playback when stitched ads are in play.
+ */
+export default class StitchedContentPlayback extends ContentState {
+
+ /*
+ * Allows state name to be logged even after minification.
+ */
+ static _getName() {
+ return 'StitchedContentPlayback';
+ }
+
+ /*
+ * For state transitions to work correctly, initialization should
+ * happen here, not in a constructor.
+ */
+ init() {
+
+ // Don't block calls to play in stitched ad players, ever.
+ this.player.ads._shouldBlockPlay = false;
+ }
+
+ /*
+ * Source change does not do anything for stitched ad players.
+ * contentchanged does not fire during ad breaks, so we don't need to
+ * worry about that.
+ */
+ onContentChanged() {
+ this.player.ads.debug(`Received contentchanged event (${this._getName()})`);
+ }
+
+ /*
+ * This is how stitched ads start.
+ */
+ startLinearAdMode() {
+ this.transitionTo(StitchedAdRoll);
+ }
+}
diff --git a/src/states/abstract/State.js b/src/states/abstract/State.js
index dd1a20a9..16cd238d 100644
--- a/src/states/abstract/State.js
+++ b/src/states/abstract/State.js
@@ -46,6 +46,7 @@ export default class State {
onPlay() {}
onPlaying() {}
onEnded() {}
+ onAdEnded() {}
onAdsReady() {
videojs.log.warn('Unexpected adsready event');
}
@@ -138,6 +139,8 @@ export default class State {
this.onNoPreroll(player);
} else if (type === 'nopostroll') {
this.onNoPostroll(player);
+ } else if (type === 'adended') {
+ this.onAdEnded(player);
}
}
diff --git a/test/index.html b/test/index.html
deleted file mode 100644
index bd330bab..00000000
--- a/test/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- videojs-contrib-ads Unit Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test/integration/lib/shared-module-hooks.js b/test/integration/lib/shared-module-hooks.js
index a19d9908..ed997f12 100644
--- a/test/integration/lib/shared-module-hooks.js
+++ b/test/integration/lib/shared-module-hooks.js
@@ -57,7 +57,7 @@ window.sharedModuleHooks = (function(){
return videojs.createTimeRange(0, 0);
};
- this.player.ads();
+ this.player.ads(this.adsOptions);
},
afterEach: function() {
diff --git a/test/integration/test.cancelContentPlay.js b/test/integration/test.cancelContentPlay.js
index 57661146..6697a15d 100644
--- a/test/integration/test.cancelContentPlay.js
+++ b/test/integration/test.cancelContentPlay.js
@@ -20,10 +20,14 @@ const restoreVideojs = function() {
};
// Run custom hooks before sharedModuleHooks, as videojs must be
-// modified before seting up the player and videojs-contrib-ads
+// modified before setting up the player and videojs-contrib-ads
QUnit.module('Cancel Content Play', {
- beforeEach: _.flow(fakeVideojs, sharedHooks.beforeEach),
- afterEach: _.flow(restoreVideojs, sharedHooks.afterEach)
+ beforeEach: _.flow(function() {
+ this.adsOptions = {};
+ }, fakeVideojs, sharedHooks.beforeEach),
+ afterEach: _.flow(function() {
+ this.adsOptions = null;
+ }, restoreVideojs, sharedHooks.afterEach)
});
QUnit.test('pauses to wait for prerolls when the plugin loads BEFORE play', function(assert) {
@@ -42,7 +46,6 @@ QUnit.test('pauses to wait for prerolls when the plugin loads BEFORE play', func
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 = sinon.spy(this.player, 'pause');
@@ -133,4 +136,47 @@ QUnit.test('content is resumed after ads if a user initiated play event is cance
this.player.trigger('play');
assert.ok(pauseSpy.callCount, 1, 'pause was not called again');
-});
\ No newline at end of file
+});
+
+// Set up contrib-ads options and run custom hooks before sharedModuleHooks, as
+// videojs must be modified before setting up the player and videojs-contrib-ads
+QUnit.module('Cancel Content Play (w/ Stitched Ads)', {
+ beforeEach: _.flow(function() {
+ this.adsOptions = {
+ stitchedAds: true
+ };
+ }, fakeVideojs, sharedHooks.beforeEach),
+ afterEach: _.flow(function() {
+ this.adsOptions = null;
+ }, restoreVideojs, sharedHooks.afterEach)
+});
+
+QUnit.test('does not pause to wait for prerolls when the plugin loads BEFORE play', function(assert) {
+ var spy = sinon.spy(this.player, 'pause');
+
+ 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, 0, 'play attempts are not paused');
+});
+
+QUnit.test('does not pause to wait for prerolls when the plugin loads AFTER play', function(assert) {
+ var pauseSpy = sinon.spy(this.player, 'pause');
+
+ this.player.paused = function() {
+ return false;
+ };
+
+ this.player.trigger('play');
+ this.clock.tick(1);
+ this.player.trigger('play');
+ this.clock.tick(1);
+ assert.equal(pauseSpy.callCount, 0, 'play attempts are not paused');
+});
diff --git a/test/integration/test.stitchedAds.js b/test/integration/test.stitchedAds.js
new file mode 100644
index 00000000..3989ccfe
--- /dev/null
+++ b/test/integration/test.stitchedAds.js
@@ -0,0 +1,137 @@
+/*
+TODO:
+* timeupdate, adtimeupdate, contenttimeupdate
+* loadstart, adloadstart, contentloadstart
+* play, adplay, contentplay
+* loadeddata, adloadeddata, contentloadeddata
+* loadedmetadata, adloadedmetadata, contentloadedmetadata
+*/
+
+import videojs from 'video.js';
+import '../../examples/stitched-ad-plugin/plugin.js';
+
+let originalTestTimeout = QUnit.config.testTimeout;
+
+QUnit.module('Events and Stitched Ads', {
+
+ before() {
+ QUnit.config.testTimeout = 30000;
+ },
+
+ beforeEach() {
+ this.video = document.createElement('video');
+
+ this.fixture = document.querySelector('#qunit-fixture');
+ this.fixture.appendChild(this.video);
+
+ this.player = videojs(this.video);
+ this.player.exampleStitchedAds();
+
+ this.player.src({
+ src: 'http://vjs.zencdn.net/v/oceans.webm',
+ type: 'video/webm'
+ });
+ },
+
+ afterEach() {
+ this.player.dispose();
+ },
+
+ after() {
+ QUnit.config.testTimeout = originalTestTimeout;
+ }
+});
+
+QUnit.test('Stitched Ads', function(assert) {
+ const done = assert.async();
+
+ const seenBeforePreroll = [];
+ const seenDuringPreroll = [];
+ const seenAfterPreroll = [];
+ const seenDuringMidroll = [];
+ const seenAfterMidroll = [];
+ let currentEventLog = seenBeforePreroll;
+
+ let events = [
+ 'suspend',
+ 'abort',
+ 'error',
+ 'emptied',
+ 'stalled',
+ 'canplay',
+ 'canplaythrough',
+ 'waiting',
+ 'seeking',
+ 'durationchange',
+ 'progress',
+ 'pause',
+ 'ratechange',
+ 'volumechange',
+ 'firstplay',
+ 'suspend',
+ 'playing',
+ 'ended'
+ ];
+
+ events = events
+ .concat(events.map(e => 'ad' + e))
+ .concat(events.map(e => 'content' + e));
+
+ const {player} = this;
+
+ player.on('adstart', () => {
+ if (currentEventLog === seenBeforePreroll) {
+ currentEventLog = seenDuringPreroll;
+ } else {
+ currentEventLog = seenDuringMidroll;
+ }
+ });
+
+ player.on('adend', () => {
+ if (currentEventLog === seenDuringPreroll) {
+ currentEventLog = seenAfterPreroll;
+ } else {
+ currentEventLog = seenAfterMidroll;
+ }
+ });
+
+ player.on(events, (e) => currentEventLog.push(e.type));
+
+ player.on(['error', 'aderror'], () => {
+ assert.ok(false, 'no errors');
+ done();
+ });
+
+ player.on('timeupdate', () => {
+ videojs.log(player.currentTime(), player.currentSrc());
+
+ if (player.currentTime() > 21) {
+ seenBeforePreroll.forEach(event => {
+ assert.ok(!/^ad/.test(event), event + ' has no ad prefix before preroll');
+ assert.ok(!/^content/.test(event), event + ' has no content prefix before preroll');
+ });
+
+ seenDuringPreroll.forEach(event => {
+ assert.ok(/^ad/.test(event), event + ' has ad prefix during preroll');
+ });
+
+ seenAfterPreroll.forEach(event => {
+ assert.ok(!/^ad/.test(event), event + ' has no ad prefix after preroll');
+ assert.ok(!/^content/.test(event), event + ' has no content prefix after preroll');
+ });
+
+ seenDuringMidroll.forEach(event => {
+ assert.ok(/^ad/.test(event), event + ' has ad prefix during midroll');
+ });
+
+ seenAfterMidroll.forEach(event => {
+ assert.ok(!/^ad/.test(event), event + ' has no ad prefix after midroll');
+ assert.ok(!/^content/.test(event), event + ' has no content prefix after midroll');
+ });
+
+ done();
+ }
+ });
+
+ player.ready(player.play);
+});
diff --git a/test/karma.conf.js b/test/karma.conf.js
index d7624168..c8fd5eb3 100644
--- a/test/karma.conf.js
+++ b/test/karma.conf.js
@@ -38,15 +38,15 @@ module.exports = function(config) {
'node_modules/es5-shim/es5-shim.js',
'node_modules/sinon/pkg/sinon.js',
'node_modules/video.js/dist/video.js',
- 'dist/videojs-contrib-ads.js',
- 'dist/videojs-contrib-ads.css',
- 'test/integration/lib/shared-module-hooks.js',
- 'test/dist/bundle.js',
+ {pattern: 'dist/videojs-contrib-ads.js', nocache: true},
+ {pattern: 'dist/videojs-contrib-ads.css', nocache: true},
+ {pattern: 'test/integration/lib/shared-module-hooks.js', nocache: true},
+ {pattern: 'test/dist/bundle.js', nocache: true},
// Test Data
{pattern: 'test/integration/lib/inventory.json', included: false, served: true},
{pattern: 'examples/basic-ad-plugin/superclip-low.webm', included: false, served: true},
- {pattern: 'test/integration/lib/testcaption.vtt', included:false, served:true}
+ {pattern: 'test/integration/lib/testcaption.vtt', included: false, served: true}
],
customLaunchers: {
travisChrome: {
diff --git a/test/unit/states/test.StitchedAdRoll.js b/test/unit/states/test.StitchedAdRoll.js
new file mode 100644
index 00000000..00a17319
--- /dev/null
+++ b/test/unit/states/test.StitchedAdRoll.js
@@ -0,0 +1,52 @@
+import {StitchedAdRoll} from '../../../src/states.js';
+import adBreak from '../../../src/adBreak.js';
+
+/*
+ * These tests are intended to be isolated unit tests for one state with all
+ * other modules mocked.
+ */
+QUnit.module('StitchedAdRoll', {
+ beforeEach: function() {
+ this.player = {
+ addClass: () => {},
+ removeClass: () => {},
+ trigger: sinon.spy(),
+ ads: {
+ _inLinearAdMode: true,
+ debug: () => {}
+ }
+ };
+
+ this.adroll = new StitchedAdRoll(this.player);
+
+ this.adBreakStartStub = sinon.stub(adBreak, 'start');
+ this.adBreakEndStub = sinon.stub(adBreak, 'end');
+ },
+
+ afterEach() {
+ this.adBreakStartStub.restore();
+ this.adBreakEndStub.restore();
+ }
+});
+
+QUnit.test('starts an ad break on init', function(assert) {
+ this.adroll.init();
+ assert.equal(this.player.ads.adType, 'stitched', 'ad type is stitched');
+ assert.equal(this.adBreakStartStub.callCount, 1, 'ad break started');
+});
+
+QUnit.test('ends an ad break on endLinearAdMode', function(assert) {
+ this.adroll.init();
+ this.adroll.endLinearAdMode();
+ assert.equal(this.adBreakEndStub.callCount, 1, 'ad break ended');
+});
+
+QUnit.test('adended during ad break leaves linear ad mode and re-triggers ended', function(assert) {
+ sinon.spy(this.adroll, 'endLinearAdMode');
+
+ this.adroll.init();
+ this.adroll.onAdEnded();
+ assert.ok(this.player.trigger.calledOnce, 'the player fired one event');
+ assert.ok(this.player.trigger.calledWith('ended'), 'the event it fired was ended');
+ assert.ok(this.adroll.endLinearAdMode.calledOnce, 'the ad roll called endLinearAdMode');
+});
diff --git a/test/unit/states/test.StitchedContentPlayback.js b/test/unit/states/test.StitchedContentPlayback.js
new file mode 100644
index 00000000..63ebb759
--- /dev/null
+++ b/test/unit/states/test.StitchedContentPlayback.js
@@ -0,0 +1,43 @@
+import {StitchedContentPlayback} from '../../../src/states.js';
+
+/*
+ * These tests are intended to be isolated unit tests for one state with all
+ * other modules mocked.
+ */
+QUnit.module('StitchedContentPlayback', {
+ beforeEach: function() {
+ this.events = [];
+ this.playTriggered = false;
+
+ this.player = {
+ paused: () => false,
+ play: () => {},
+ trigger: (event) => {
+ this.events.push(event);
+ },
+ ads: {
+ debug: () => {},
+ _contentHasEnded: false,
+ _shouldBlockPlay: true
+ }
+ };
+
+ this.stitchedContentPlayback = new StitchedContentPlayback(this.player);
+ this.stitchedContentPlayback.transitionTo = (newState) => {
+ this.newState = newState.name;
+ };
+ }
+});
+
+QUnit.test('transitions to StitchedAdRoll when startLinearAdMode is called', function(assert) {
+ this.stitchedContentPlayback.init();
+ this.stitchedContentPlayback.startLinearAdMode();
+ assert.equal(this.newState, 'StitchedAdRoll', 'transitioned to StitchedAdRoll');
+});
+
+QUnit.test('sets _shouldBlockPlay to false on init', function(assert) {
+ assert.equal(this.player.ads._shouldBlockPlay, true);
+
+ this.stitchedContentPlayback.init();
+ assert.equal(this.player.ads._shouldBlockPlay, false);
+});
diff --git a/test/unit/test.ads.js b/test/unit/test.ads.js
index 119a9624..b3b2f8a4 100644
--- a/test/unit/test.ads.js
+++ b/test/unit/test.ads.js
@@ -1,3 +1,4 @@
+import videojs from 'video.js';
import getAds from '../../src/ads.js';
QUnit.module('Ads Object', {
@@ -47,6 +48,23 @@ QUnit.module('Ads Object', {
assert.equal(this.player.ads.isLive(this.player), false);
});
+ QUnit.test('stitchedAds', function(assert) {
+ assert.notOk(this.player.ads.stitchedAds());
+
+ this.player.ads.settings.stitchedAds = true;
+
+ assert.ok(this.player.ads.stitchedAds());
+
+ sinon.spy(videojs.log, 'warn');
+ this.player.ads.stitchedAds(false);
+
+ assert.ok(videojs.log.warn.calledOnce, 'using as a setter is deprecated');
+ assert.notOk(this.player.ads.stitchedAds());
+ assert.notOk(this.player.ads.settings.stitchedAds);
+
+ videojs.log.warn.restore();
+ });
+
QUnit.test('shouldPlayContentBehindAd', function(assert) {
// liveCuePoints true + finite duration
@@ -125,4 +143,20 @@ QUnit.module('Ads Object', {
videojs.browser.IS_ANDROID = true;
assert.equal(this.player.ads.shouldPlayContentBehindAd(this.player), false);
});
+
+ QUnit.test('shouldTakeSnapshots', function(assert) {
+ this.player.ads.shouldPlayContentBehindAd = () => false;
+ this.player.ads.stitchedAds = () => false;
+
+ assert.ok(this.player.ads.shouldTakeSnapshots());
+
+ this.player.ads.shouldPlayContentBehindAd = () => true;
+
+ assert.notOk(this.player.ads.shouldTakeSnapshots());
+
+ this.player.ads.shouldPlayContentBehindAd = () => false;
+ this.player.ads.stitchedAds = () => true;
+
+ assert.notOk(this.player.ads.shouldTakeSnapshots());
+ });
});
diff --git a/test/unit/test.redispatch.js b/test/unit/test.redispatch.js
index 6861b47d..edf59042 100644
--- a/test/unit/test.redispatch.js
+++ b/test/unit/test.redispatch.js
@@ -118,4 +118,4 @@ QUnit.test('play events in different states', function(assert) {
this.player.ads._playRequested = true;
assert.strictEqual(this.redispatch('play'), 'adplay',
'should be adplay when in an ad break');
-});
\ No newline at end of file
+});