diff --git a/README.md b/README.md
index e388d06..21a8589 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,49 @@
# FT Video [![Circle CI](https://circleci.com/gh/Financial-Times/o-video.svg?style=svg)](https://circleci.com/gh/Financial-Times/o-video)
-Creates a video player and attaches analytics
+Creates a video player and attaches analytics. Also supports pre roll ads.
## Usage
Create an element of the format e.g.
-
-
-Where
-
- * `data-o-video-source['brightcove']` Source of the video (currently only accepts `Brightcove`)
- * `data-o-video-id` Source's ID of the video
+```html
+
+```
In JS
- var oVideo = require('o-video');
- var opts = {
- optimumWidth: 710,
+```js
+ const OVideo = require('o-video');
+ const opts = {
+ id: 4165329773001,
+ source: "brightcove",
+ optimumwidth: 710,
placeholder: true,
- classes: ['video'],
- selector: '.js-video'
+ classes: ['video']
};
- oVideo.init(opts);
+ const video = new OVideo(document.body, opts);
+```
+
+### Config
Where `opts` is an optional object with properties
- * `optimumWidth` [`Number`] The optimum width of the video, used when there are multiple video renditions available to
+ * `id` [`Number`] Source's ID of the video
+ * `source` [`String`] Source of the video (currently only accepts `brightcove`)
+ * `optimumwidth` [`Number`] The optimum width of the video, used when there are multiple video renditions available to
decide which to display (the smallest one that's at least as large as this width, if it exists)
* `placeholder` [`Boolean`] Show just the poster image, load (and play) video on click
- * `placeholderTitle` [`Boolean`] Show just the title as an overlay on the placeholder
+ * `placeholdertitle` [`Boolean`] Show just the title as an overlay on the placeholder
* `classes` [`Array`] Classes to add to the video (and placeholder) element
- * `selector` [`String`] Selector to use to find the `o-video` elements. Appended with
- `:not([data-o-video-js])[data-o-component~="o-video"]`. Defaults to `*`.
- `optimumWidth`, `palceholder` and `classes` can also be set in the markup with an attribute of the form `data-o-video-opts-*`, e.g.
- `data-o-video-opts-optimum-width="300"`. These trump options supplied to the `init` method.
+The config options can also be set as data attribute to instantiate the module declaratively:
- If the data is available, it can also be passed through in an attribute (e.g. `data-o-video-opts-data="{ "videoStillURL": ... }"`)
- to save the browser an HTTP request
+```html
+
+```
## Testing
diff --git a/demos/src/demo.js b/demos/src/demo.js
index 0c1a799..c28b2c4 100644
--- a/demos/src/demo.js
+++ b/demos/src/demo.js
@@ -1,5 +1,6 @@
/* global console */
-const oVideo = require('../../main');
-oVideo.init({
- advertising: true
+import './../../main.js';
+
+document.addEventListener("DOMContentLoaded", function() {
+ document.dispatchEvent(new CustomEvent('o.DOMContentLoaded'));
});
diff --git a/demos/src/placeholder.mustache b/demos/src/placeholder.mustache
new file mode 100644
index 0000000..7bf7887
--- /dev/null
+++ b/demos/src/placeholder.mustache
@@ -0,0 +1,7 @@
+
diff --git a/demos/src/video.mustache b/demos/src/video.mustache
index 90c27bd..cc25c6e 100644
--- a/demos/src/video.mustache
+++ b/demos/src/video.mustache
@@ -1,5 +1,6 @@
+ data-o-video-id="4165329773001"
+ data-o-video-advertising="true">
diff --git a/demos/src/videojs.mustache b/demos/src/videojs.mustache
deleted file mode 100644
index 8a718cd..0000000
--- a/demos/src/videojs.mustache
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/main.js b/main.js
index 9aea29f..26f7ff8 100644
--- a/main.js
+++ b/main.js
@@ -1,61 +1,10 @@
-const factory = require('./src/models/video-factory');
+import Video from './src/video.js';
-function loadAdsLibrary() {
- return new Promise((resolve, reject) => {
- const googleSdkScript = document.createElement('script');
- googleSdkScript.setAttribute('type', 'text/javascript');
- googleSdkScript.setAttribute('src', `//imasdk.googleapis.com/js/sdkloader/ima3.js`);
- googleSdkScript.setAttribute('async', true);
- googleSdkScript.setAttribute('defer', true);
- document.getElementsByTagName("head")[0].appendChild(googleSdkScript);
-
- googleSdkScript.addEventListener('load', () => {
- resolve();
- });
-
- googleSdkScript.addEventListener('error', () => {
- reject();
- });
- });
-}
-
-function loadVideos(options) {
- const videoPromises = [].map.call(options.context.querySelectorAll(options.selector + ':not([data-o-video-js])[data-o-component~="o-video"]'), videoEl => {
- return factory(videoEl, options)
- .init()
- // don't fail all if a video errors
- .catch(() => { });
- });
-
- return Promise.all(videoPromises);
-}
-
-function init(opts) {
- const options = opts || {};
- const defaultOpts = {
- context: document.body,
- selector: '*'
- };
-
- for (let defaultOpt in defaultOpts) {
- if (defaultOpts.hasOwnProperty(defaultOpt) && !options.hasOwnProperty(defaultOpt)) {
- options[defaultOpt] = defaultOpts[defaultOpt];
- }
- }
-
- const librariesLoaded = options.advertising ? loadAdsLibrary() : Promise.resolve();
- options.context = options.context instanceof HTMLElement ? options.context : document.querySelector(opts.context);
-
- return librariesLoaded.then(() => {
- return loadVideos(options);
- }, () => {
- options.ads = false;
- return loadVideos(options);
- });
+const constructAll = () => {
+ Video.init();
+ document.removeEventListener('o.DOMContentLoaded', constructAll);
};
-module.exports = {
- init,
- factory,
- _loadAdsLibrary: loadAdsLibrary
-};
+document.addEventListener('o.DOMContentLoaded', constructAll);
+
+export default Video;
diff --git a/main.scss b/main.scss
index f3f7eb1..1bde1a8 100644
--- a/main.scss
+++ b/main.scss
@@ -68,57 +68,5 @@ $_o-video_applied: false !default;
}
}
- // Videojs stuff
- .o-video--videojs {
- width: 100%;
- height: 100%;
- border: 0;
- display: block;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- }
-
- [data-o-video-player="videojs"] .o-video--videojs {
- position: absolute;
- }
-
- // HACK: Liberal use of `!important` to override videojs styling.
- .vjs-big-play-button {
- position: absolute !important;
- width: 100% !important;
- height: 100% !important;
- top: 0 !important;
- left: 0 !important;
- padding: 0;
- background: transparent !important;
- border-radius: 0;
- text-indent: -9999px;
- cursor: pointer;
- border: 0;
- z-index: 1;
-
- &:after {
- @include oIconsGetIcon('play', #fcfcfc, 40);
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- margin-top: -20px;
- margin-left: -20px;
- opacity: 1;
- transition: opacity 0.1s;
- background: #000000;
- background-color: rgba(0, 0, 0, 0.5);
- border-radius: 20px;
- }
-
- &:hover:after {
- opacity: 0.5;
- }
- }
-
$_o-video_applied: true !global;
}
diff --git a/origami.json b/origami.json
index 5c1edb2..e74e520 100644
--- a/origami.json
+++ b/origami.json
@@ -19,13 +19,7 @@
"name": "placeholder",
"description": "Placeholder",
"expanded": true,
- "js": "demos/src/placeholder.js"
- },
- {
- "name": "videojs",
- "description": "Video.js",
- "expanded": true,
- "template": "demos/src/videojs.mustache"
+ "template": "demos/src/placeholder.mustache"
}
]
}
diff --git a/src/ads.js b/src/ads.js
new file mode 100644
index 0000000..34ddce6
--- /dev/null
+++ b/src/ads.js
@@ -0,0 +1,195 @@
+/* global google */
+
+class VideoAds {
+ constructor(video) {
+ this.video = video;
+ }
+
+ loadAdsLibrary() {
+ return new Promise((resolve, reject) => {
+ if (document.querySelector('[src="//imasdk.googleapis.com/js/sdkloader/ima3.js"]')) {
+ resolve();
+ }
+ const googleSdkScript = document.createElement('script');
+ googleSdkScript.setAttribute('type', 'text/javascript');
+ googleSdkScript.setAttribute('src', `//imasdk.googleapis.com/js/sdkloader/ima3.js`);
+ googleSdkScript.setAttribute('async', true);
+ googleSdkScript.setAttribute('defer', true);
+ document.getElementsByTagName("head")[0].appendChild(googleSdkScript);
+
+ googleSdkScript.addEventListener('load', () => {
+ resolve();
+ });
+
+ googleSdkScript.addEventListener('error', () => {
+ reject();
+ });
+ });
+ }
+
+ getVideoBrand() {
+ if (!this.video.videoData || !this.video.videoData.tags || this.video.videoData.tags.length === 0) {
+ return false;
+ } else {
+ let filtered = this.video.videoData.tags.filter(val => val.toLowerCase().indexOf('brand:') !== -1);
+ if (filtered.length > 0) {
+ try {
+ // when we target the value in the ad server, we only want to target actual brand name, so we strip out "brand:" part of the string
+ return filtered.pop().substring(6);
+ }
+ catch (e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ setUpAds() {
+ this.adContainerEl = document.createElement('div');
+ this.video.containerEl.appendChild(this.adContainerEl);
+ this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adContainerEl, this.video.videoEl);
+
+ // Create ads loader.
+ this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
+
+ // Sets up bindings for all Ad related handlers
+ this.adsManagerLoadedHandler = this.adsManagerLoadedHandler.bind(this);
+ this.adErrorHandler = this.adErrorHandler.bind(this);
+ this.adEventHandler = this.adEventHandler.bind(this);
+ this.contentPauseRequestHandler = this.contentPauseRequestHandler.bind(this);
+ this.contentResumeRequestHandler = this.contentResumeRequestHandler.bind(this);
+
+ // Listen and respond to ads loaded and error events.
+ this.adsLoader.addEventListener(
+ google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
+ this.adsManagerLoadedHandler,
+ false);
+ this.adsLoader.addEventListener(
+ google.ima.AdErrorEvent.Type.AD_ERROR,
+ this.adErrorHandler,
+ false);
+
+ // Request video ads.
+ const adsRequest = new google.ima.AdsRequest();
+ let advertisingUrl = `http://pubads.g.doubleclick.net/gampad/ads?env=vp&gdfp_req=1&impl=s&output=xml_vast2&iu=${this.video.targeting.site}&sz=${this.video.targeting.sizes}&unviewed_position_start=1&scp=pos%3D${this.video.targeting.position}&ttid=${this.video.targeting.videoId}`;
+
+ const brand = this.getVideoBrand();
+ if (brand) {
+ advertisingUrl += `&brand=${encodeURIComponent(brand)}`;
+ }
+
+ adsRequest.adTagUrl = advertisingUrl;
+
+ // Specify the linear and nonlinear slot sizes. This helps the SDK to
+ // select the correct creative if multiple are returned.
+ adsRequest.linearAdSlotWidth = 592;
+ adsRequest.linearAdSlotHeight = 333;
+
+ adsRequest.nonLinearAdSlotWidth = 592;
+ adsRequest.nonLinearAdSlotHeight = 150;
+
+ this.adsLoader.requestAds(adsRequest);
+ }
+
+ adsManagerLoadedHandler(adsManagerLoadedEvent) {
+ // If the video has started before the ad loaded, don't load the ad
+ if (this.video.videoEl.played.length > 0) {
+ return;
+ }
+ // Get the ads manager.
+ const adsRenderingSettings = new google.ima.AdsRenderingSettings();
+ adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
+ this.adsManager = adsManagerLoadedEvent.getAdsManager(this.video.videoEl, adsRenderingSettings);
+
+ // Add listeners to the required events.
+ this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, this.adErrorHandler);
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, this.contentPauseRequestHandler);
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, this.contentResumeRequestHandler);
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, this.adEventHandler);
+
+ // Listen to any additional events, if necessary.
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, this.adEventHandler);
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, this.adEventHandler);
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, this.adEventHandler);
+
+ this.playAdEventHandler = this.playAdEventHandler.bind(this);
+ this.video.videoEl.addEventListener('play', this.playAdEventHandler);
+ }
+
+ playAdEventHandler() {
+ // Sets the styling now so the ad occupies the space of the video
+ this.adContainerEl.classList.add('o-video__ad');
+ // Initialize the video. Must be done via a user action on mobile devices.
+ this.video.videoEl.load();
+ this.adDisplayContainer.initialize();
+
+ try {
+ // Initialize the ads manager. Ad rules playlist will start at this time.
+ this.adsManager.init(this.video.videoEl.clientWidth, this.video.videoEl.clientHeight, google.ima.ViewMode.NORMAL);
+ // Call play to start showing the ad. Single video and overlay ads will
+ // start at this time; the call will be ignored for ad rules.
+ this.adsManager.start();
+ } catch (adError) {
+ // An error may be thrown if there was a problem with the VAST response.
+ this.video.videoEl.play();
+ }
+
+ this.video.videoEl.removeEventListener('play', this.playAdEventHandler);
+ }
+
+ adEventHandler(adEvent) {
+ // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
+ // don't have ad object associated.
+ const ad = adEvent.getAd();
+ let intervalTimer;
+ switch (adEvent.type) {
+ case google.ima.AdEvent.Type.LOADED:
+ // This is the first event sent for an ad - it is possible to
+ // determine whether the ad is a video ad or an overlay.
+ if (!ad.isLinear()) {
+ // Position AdDisplayContainer correctly for overlay.
+ // Use ad.width and ad.height.
+ this.video.videoEl.play();
+ }
+ break;
+ case google.ima.AdEvent.Type.STARTED:
+ // This event indicates the ad has started - the video player
+ // can adjust the UI, for example display a pause button and
+ // remaining time.
+ if (ad.isLinear()) {
+ // For a linear ad, a timer can be started to poll for
+ // the remaining time.
+ // TODO: We could use this to add a skip ad button
+ intervalTimer = setInterval(() => {
+ // Currently not used
+ // const remainingTime = this.adsManager.getRemainingTime();
+ }, 300); // every 300ms
+ }
+ break;
+ case google.ima.AdEvent.Type.COMPLETE:
+ this.adContainerEl.style.display = 'none';
+ if (ad.isLinear()) {
+ clearInterval(intervalTimer);
+ }
+ break;
+ }
+ }
+
+ adErrorHandler() {
+ // Todo: Handle the error logging.
+ this.adsManager.destroy();
+ }
+
+ contentPauseRequestHandler() {
+ this.video.videoEl.pause();
+ }
+
+ contentResumeRequestHandler() {
+ this.video.containerEl.removeChild(this.adContainerEl);
+ this.video.videoEl.play();
+ }
+}
+
+export default VideoAds;
diff --git a/src/libs/get-appropriate-rendition.js b/src/helpers/get-rendition.js
similarity index 86%
rename from src/libs/get-appropriate-rendition.js
rename to src/helpers/get-rendition.js
index 86cc5bd..663ea38 100644
--- a/src/libs/get-appropriate-rendition.js
+++ b/src/helpers/get-rendition.js
@@ -1,6 +1,6 @@
-const supportedFormats = require('../libs/supported-formats');
+import supportedFormats from './supported-formats';
-module.exports = (renditions, options) => {
+function getRendition(renditions, options) {
// allow mocking of supported formats module
const opts = options || {};
const width = opts.width;
@@ -30,3 +30,5 @@ module.exports = (renditions, options) => {
return appropriateRendition || orderedRenditions.shift();
};
+
+export default getRendition;
diff --git a/src/libs/supported-formats.js b/src/helpers/supported-formats.js
similarity index 93%
rename from src/libs/supported-formats.js
rename to src/helpers/supported-formats.js
index 5ea4789..eb7a81e 100644
--- a/src/libs/supported-formats.js
+++ b/src/helpers/supported-formats.js
@@ -23,4 +23,4 @@ if (testEl.canPlayType) {
} catch(e) { }
}
-module.exports = supportedFormats;
+export default supportedFormats;
diff --git a/src/models/brightcove.js b/src/models/brightcove.js
deleted file mode 100644
index fda448e..0000000
--- a/src/models/brightcove.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/* global fetch, google */
-const crossDomainFetch = require('o-fetch-jsonp').crossDomainFetch;
-const Video = require('./video');
-const getAppropriateRendition = require('../libs/get-appropriate-rendition');
-
-let currentlyPlayingVideo = null;
-let requestedVideo = null;
-
-const pauseOtherVideos = (video) => {
- requestedVideo = video;
- if(currentlyPlayingVideo && currentlyPlayingVideo !== requestedVideo){
- currentlyPlayingVideo.pause();
- }
-
- currentlyPlayingVideo = video;
-};
-
-const clearCurrentlyPlaying = () => {
- if(currentlyPlayingVideo !== requestedVideo){
- currentlyPlayingVideo = null;
- }
-};
-
-
-const eventListener = (video, ev) => {
- const event = new CustomEvent('oTracking.event', {
- detail: {
- action: 'media',
- advertising: video.opts.advertising,
- category: 'video',
- event: ev.type,
- mediaType: 'video',
- contentId: video.id,
- progress: video.getProgress(),
- },
- bubbles: true
- });
- document.body.dispatchEvent(event);
-};
-
-const addEvents = (video, events) => {
- events.forEach(event => {
- video.el.addEventListener(event, eventListener.bind(this, video));
- });
-};
-
-// use the image resizing service, if width supplied
-const updatePosterUrl = (posterImage, width) => {
- let url = `https://image.webservices.ft.com/v1/images/raw/${encodeURIComponent(posterImage)}?source=o-video`;
- if (width) {
- url += `&fit=scale-down&width=${width}`;
- }
- return url;
-};
-
-class Brightcove extends Video {
- constructor(el, opts) {
- super(el, opts);
- this.targeting = {
- site: '/5887/ft.com',
- position: 'video',
- sizes: '592x333|400x225',
- videoId: this.id
- };
- }
-
- getData() {
- const dataPromise = this.opts.data ? Promise.resolve(this.opts.data) : crossDomainFetch(`//next-video.ft.com/api/${this.id}`)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw Error('Brightcove responded with a ' + response.status + ' (' + response.statusText + ') for id ' + this.id);
- }
- });
-
- return dataPromise.then(data => {
- this.brightcoveData = data;
- this.posterImage = updatePosterUrl(data.videoStillURL, this.opts.optimumWidth);
- this.rendition = getAppropriateRendition(data.renditions);
- });
- }
-
- renderVideo() {
- if (this.rendition) {
- if (this.opts.placeholder) {
- this.addPlaceholder();
- } else {
- this.addVideo();
- }
- }
- return this;
- }
-
- init() {
- return this.getData().then(() => this.renderVideo());
- }
-
- info() {
- const date = new Date(+this.brightcoveData.publishedDate);
- return {
- posterImage: this.posterImage,
- id: this.brightcoveData.id,
- length: this.brightcoveData.length,
- longDescription: this.brightcoveData.longDescription,
- name: this.brightcoveData.name,
- publishedDate: date.toISOString(),
- publishedDateReadable: date.toUTCString(),
- shortDescription: this.brightcoveData.shortDescription,
- tags: this.brightcoveData.tags,
- };
- }
-
- setUpAds() {
- this.adContainerEl = document.createElement('div');
- this.containerEl.appendChild(this.adContainerEl);
- this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adContainerEl, this.el);
-
- // Create ads loader.
- this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
-
- // Sets up bindings for all Ad related handlers
- this.adsManagerLoadedHandler = this.adsManagerLoadedHandler.bind(this);
- this.adErrorHandler = this.adErrorHandler.bind(this);
- this.adEventHandler = this.adEventHandler.bind(this);
- this.contentPauseRequestHandler = this.contentPauseRequestHandler.bind(this);
- this.contentResumeRequestHandler = this.contentResumeRequestHandler.bind(this);
-
- // Listen and respond to ads loaded and error events.
- this.adsLoader.addEventListener(
- google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
- this.adsManagerLoadedHandler,
- false);
- this.adsLoader.addEventListener(
- google.ima.AdErrorEvent.Type.AD_ERROR,
- this.adErrorHandler,
- false);
-
- // Request video ads.
- const adsRequest = new google.ima.AdsRequest();
- let advertisingUrl = `http://pubads.g.doubleclick.net/gampad/ads?env=vp&gdfp_req=1&impl=s&output=xml_vast2&iu=${this.targeting.site}&sz=${this.targeting.sizes}&unviewed_position_start=1&scp=pos%3D${this.targeting.position}&ttid=${this.targeting.videoId}`;
- if(this.targeting.brand) {
- advertisingUrl += `&brand=${encodeURIComponent(this.targeting.brand)}`;
- }
-
- adsRequest.adTagUrl = advertisingUrl;
-
- // Specify the linear and nonlinear slot sizes. This helps the SDK to
- // select the correct creative if multiple are returned.
- adsRequest.linearAdSlotWidth = 592;
- adsRequest.linearAdSlotHeight = 333;
-
- adsRequest.nonLinearAdSlotWidth = 592;
- adsRequest.nonLinearAdSlotHeight = 150;
-
- this.adsLoader.requestAds(adsRequest);
- }
-
- addVideo() {
- this.el = document.createElement('video');
- this.el.setAttribute('controls', true);
- this.el.setAttribute('poster', this.posterImage);
- this.el.setAttribute('src', this.rendition.url);
- this.el.className = Array.isArray(this.classes) ? this.classes.join(' ') : this.classes;
- this.containerEl.classList.add('o-video--player');
- this.containerEl.appendChild(this.el);
- addEvents(this, ['play', 'pause', 'ended']);
- this.el.addEventListener('playing', () => pauseOtherVideos(this.el));
- this.el.addEventListener('suspend', clearCurrentlyPlaying);
- this.el.addEventListener('ended', clearCurrentlyPlaying);
-
- if (this.opts.advertising) {
- this.setUpAds();
- }
- }
-
- addPlaceholder() {
- this.placeholderEl = document.createElement('img');
- this.placeholderEl.setAttribute('src', this.posterImage);
- this.placeholderEl.classList.add('o-video__placeholder');
- this.containerEl.appendChild(this.placeholderEl);
-
- let titleEl;
- if (this.opts.placeholderTitle) {
- titleEl = document.createElement('div');
- titleEl.className = 'o-video__title';
- titleEl.textContent = this.brightcoveData.name;
- this.containerEl.appendChild(titleEl);
- }
-
- if (this.opts.playButton) {
- const playButtonEl = document.createElement('button');
- playButtonEl.className = 'o-video__play-button';
-
- const playButtonTextEl = document.createElement('dd');
- playButtonTextEl.className = 'o-video__play-button-text';
- playButtonTextEl.textContent = 'Play video';
- playButtonEl.appendChild(playButtonTextEl);
-
- const playIconEl = document.createElement('i');
- playIconEl.className = 'o-video__play-button-icon';
- playButtonEl.appendChild(playIconEl);
-
- this.containerEl.appendChild(playButtonEl);
-
- playButtonEl.addEventListener('click', () => {
- this.containerEl.removeChild(playButtonEl);
- if (titleEl) {
- this.containerEl.removeChild(titleEl);
- }
- this.containerEl.removeChild(this.placeholderEl);
-
- this.el.style.display = 'block';
- this.el.play();
- this.el.focus();
- });
- }
-
- // Adds video soon so ads can start loading
- this.addVideo();
- // Hide it so it doesn't flash until the placeholder image loads
- this.el.style.display = 'none';
- }
-
- getProgress() {
- return this.el.duration ? parseInt(100 * this.el.currentTime / this.el.duration, 10) : 0;
- }
-
- playAdEventHandler() {
- // Sets the styling now so the ad occupies the space of the video
- this.adContainerEl.classList.add('o-video__ad');
- // Initialize the video. Must be done via a user action on mobile devices.
- this.el.load();
- this.adDisplayContainer.initialize();
-
- try {
- // Initialize the ads manager. Ad rules playlist will start at this time.
- this.adsManager.init(this.el.clientWidth, this.el.clientHeight, google.ima.ViewMode.NORMAL);
- // Call play to start showing the ad. Single video and overlay ads will
- // start at this time; the call will be ignored for ad rules.
- this.adsManager.start();
- } catch (adError) {
- // An error may be thrown if there was a problem with the VAST response.
- this.el.play();
- }
-
- this.el.removeEventListener('play', this.playAdEventHandler);
- }
-
- adsManagerLoadedHandler(adsManagerLoadedEvent) {
- // If the video has started before the ad loaded, don't load the ad
- if (this.el.played.length > 0) {
- return;
- }
- // Get the ads manager.
- const adsRenderingSettings = new google.ima.AdsRenderingSettings();
- adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
- this.adsManager = adsManagerLoadedEvent.getAdsManager(this.el, adsRenderingSettings);
-
- // Add listeners to the required events.
- this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, this.adErrorHandler);
- this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, this.contentPauseRequestHandler);
- this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, this.contentResumeRequestHandler);
- this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, this.adEventHandler);
-
- // Listen to any additional events, if necessary.
- this.adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, this.adEventHandler);
- this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, this.adEventHandler);
- this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, this.adEventHandler);
-
- this.playAdEventHandler = this.playAdEventHandler.bind(this);
- this.el.addEventListener('play', this.playAdEventHandler);
- }
-
- adEventHandler(adEvent) {
- // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
- // don't have ad object associated.
- const ad = adEvent.getAd();
- let intervalTimer;
- switch (adEvent.type) {
- case google.ima.AdEvent.Type.LOADED:
- // This is the first event sent for an ad - it is possible to
- // determine whether the ad is a video ad or an overlay.
- if (!ad.isLinear()) {
- // Position AdDisplayContainer correctly for overlay.
- // Use ad.width and ad.height.
- this.el.play();
- }
- break;
- case google.ima.AdEvent.Type.STARTED:
- // This event indicates the ad has started - the video player
- // can adjust the UI, for example display a pause button and
- // remaining time.
- if (ad.isLinear()) {
- // For a linear ad, a timer can be started to poll for
- // the remaining time.
- intervalTimer = setInterval(() => {
- // Currently not used
- // const remainingTime = this.adsManager.getRemainingTime();
- }, 300); // every 300ms
- }
- break;
- case google.ima.AdEvent.Type.COMPLETE:
- this.adContainerEl.style.display = 'none';
- if (ad.isLinear()) {
- clearInterval(intervalTimer);
- }
- break;
- }
- }
-
- adErrorHandler() {
- // Handle the error logging.
- this.adsManager.destroy();
- }
-
- contentPauseRequestHandler() {
- this.el.pause();
- }
-
- contentResumeRequestHandler() {
- this.containerEl.removeChild(this.adContainerEl);
- this.el.play();
- }
-}
-
-module.exports = Brightcove;
diff --git a/src/models/video-factory.js b/src/models/video-factory.js
deleted file mode 100644
index 8053305..0000000
--- a/src/models/video-factory.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const Video = require('./video');
-const Brightcove = require('./brightcove');
-const VideoJsPlayer = require('./videojs');
-
-module.exports = (el, opts) => {
- const source = el.getAttribute('data-o-video-source').toLowerCase();
- const player = (el.getAttribute('data-o-video-player') || 'html5').toLowerCase();
- if (source === 'brightcove') {
- if (player === 'videojs') {
- return new VideoJsPlayer(el, opts);
- }
- return new Brightcove(el, opts);
- } else {
- return new Video(el, opts);
- }
-
-};
diff --git a/src/models/video.js b/src/models/video.js
deleted file mode 100644
index 5585025..0000000
--- a/src/models/video.js
+++ /dev/null
@@ -1,41 +0,0 @@
-class Video {
- constructor(el, opts) {
- this.containerEl = el;
- const defaultOpts = {
- advertising: false,
- classes: [],
- optimumWidth: null,
- placeholder: false,
- placeholderTitle: false,
- playButton: true,
- data: null
- };
- this.opts = {};
- Object.keys(defaultOpts).forEach(optionName => {
- const attributeName = optionName.replace(/[A-Z]/g, function (match) {
- return '-' + match.toLowerCase();
- });
- const optionAttribute = this.containerEl.getAttribute('data-o-video-opts-' + attributeName);
- if (optionAttribute) {
- // parse as JSON, if 'data' attribute
- this.opts[optionName] = optionName === 'data' ? JSON.parse(optionAttribute) : optionAttribute;
- } else if (opts && typeof opts[optionName] !== 'undefined') {
- this.opts[optionName] = opts[optionName];
- } else {
- this.opts[optionName] = defaultOpts[optionName];
- }
- });
- this.classes = typeof this.opts.classes === 'string' ? this.opts.classes.split(' ') : this.opts.classes.slice();
- this.classes.push('o-video__video');
- this.id = el.getAttribute('data-o-video-id');
- this.el;
- this.placeholderEl;
- this.containerEl.setAttribute('data-o-video-js', '');
- }
-
- init() {
- return Promise.resolve(this);
- }
-}
-
-module.exports = Video;
diff --git a/src/models/videojs.js b/src/models/videojs.js
deleted file mode 100644
index c7a65b0..0000000
--- a/src/models/videojs.js
+++ /dev/null
@@ -1,258 +0,0 @@
-/* global fetch, videojs */
-
-const crossDomainFetch = require('o-fetch-jsonp').crossDomainFetch;
-const Video = require('./video');
-const getAppropriateRendition = require('../libs/get-appropriate-rendition');
-
-let currentlyPlayingVideo = null;
-let requestedVideo = null;
-let videoJsPromise;
-let videoJsPluginsPromise;
-let videoElementIdOrder = 0;
-let advertising;
-
-const pauseOtherVideos = (video) => {
- requestedVideo = video;
- if(currentlyPlayingVideo && currentlyPlayingVideo !== requestedVideo){
- currentlyPlayingVideo.pause();
- }
-
- currentlyPlayingVideo = video;
-};
-
-const ensureVideoJsLibraryLoaded = () => {
- if (videoJsPromise) {
- return videoJsPromise;
- }
-
- const videojsScript = document.createElement('script');
- videojsScript.setAttribute('type', 'text/javascript');
- videojsScript.setAttribute('src', `//vjs.zencdn.net/5.9.2/video.min.js`);
- videojsScript.setAttribute('async', true);
- videojsScript.setAttribute('defer', true);
- document.getElementsByTagName("head")[0].appendChild(videojsScript);
-
- const videojsStyles = document.createElement('link');
- videojsStyles.setAttribute('rel', 'stylesheet');
- videojsStyles.setAttribute('type', 'text/css');
- videojsStyles.setAttribute('href', '//vjs.zencdn.net/5.9.2/video-js.min.css');
- document.getElementsByTagName('head')[0].appendChild(videojsStyles);
-
- videoJsPromise = new Promise(resolve => {
- videojsScript.addEventListener('load', () => {
- resolve();
- });
- });
-
- return videoJsPromise;
-};
-
-const ensureVideoJsPluginsAreLoaded = () => {
- if(videoJsPluginsPromise) {
- return videoJsPluginsPromise;
- }
-
- if(advertising) {
- const videoJsAdPluginsScript = document.createElement('script');
- videoJsAdPluginsScript.setAttribute('type', 'text/javascript');
- videoJsAdPluginsScript.setAttribute('src', `https://next-geebee.ft.com/assets/videojs/videojs-plugins.min.js`);
- videoJsAdPluginsScript.setAttribute('async', true);
- videoJsAdPluginsScript.setAttribute('defer', true);
- document.getElementsByTagName("head")[0].appendChild(videoJsAdPluginsScript);
-
- let videoJsAdPluginsScriptPromise = new Promise(pluginsLoaded => {
- videoJsAdPluginsScript.addEventListener('load', () => {
- pluginsLoaded();
- });
- });
-
- const videoJsAdPluginStyles = document.createElement('link');
- videoJsAdPluginStyles.setAttribute('rel', 'stylesheet');
- videoJsAdPluginStyles.setAttribute('type', 'text/css');
- videoJsAdPluginStyles.setAttribute('href', 'https://next-geebee.ft.com/assets/videojs/videojs-plugins.min.css');
- document.getElementsByTagName('head')[0].appendChild(videoJsAdPluginStyles);
- videoJsPluginsPromise = Promise.all([videoJsAdPluginsScriptPromise]);
- return videoJsPluginsPromise;
- } else {
- return Promise.resolve();
- }
-};
-
-const ensureAllScriptsAreLoaded = () => {
- return ensureVideoJsLibraryLoaded().then(() => {
- return ensureVideoJsPluginsAreLoaded();
- });
-};
-
-
-// use the image resizing service, if width supplied
-const updatePosterUrl = (posterImage, width) => {
- let url = `https://image.webservices.ft.com/v1/images/raw/${encodeURIComponent(posterImage)}?source=o-video`;
- if (width) {
- url += `&fit=scale-down&width=${width}`;
- }
- return url;
-};
-
-class VideoJsPlayer extends Video {
- constructor(el, opts) {
- advertising = opts && opts['advertising'] ? true : false;
- ensureAllScriptsAreLoaded();
- super(el, opts);
- this.targeting = {
- site: '/5887/ft.com',
- position: 'video',
- sizes: '592x333|400x225',
- videoId: this.id
- };
- }
-
- getData() {
- const dataPromise = this.opts.data ? Promise.resolve(this.opts.data) : crossDomainFetch(`//next-video.ft.com/api/${this.id}`)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw Error('Brightcove responded with a ' + response.status + ' (' + response.statusText + ') for id ' + this.id);
- }
- });
-
- return dataPromise.then(data => {
- this.brightcoveData = data;
- this.posterImage = updatePosterUrl(data.videoStillURL, this.opts.optimumWidth);
- this.rendition = getAppropriateRendition(data.renditions);
- this.targeting.brand = this.getVideoBrand();
- });
- }
-
- renderVideo() {
- if (this.rendition) {
- if (this.opts.placeholder) {
- this.addPlaceholder();
- } else {
- this.addVideo();
- }
- }
- return this;
- }
-
- init() {
- const initPromise = this.getData().then(() => this.renderVideo());
- return Promise.all([initPromise, videoJsPromise]);
- }
-
- info() {
- const date = new Date(+this.brightcoveData.publishedDate);
- return {
- posterImage: this.posterImage,
- id: this.brightcoveData.id,
- length: this.brightcoveData.length,
- longDescription: this.brightcoveData.longDescription,
- name: this.brightcoveData.name,
- publishedDate: date.toISOString(),
- publishedDateReadable: date.toUTCString(),
- shortDescription: this.brightcoveData.shortDescription,
- tags: this.brightcoveData.tags,
- };
- }
-
-
- addVideo() {
- let videoIdProperty = 'test-video-' + videoElementIdOrder++;
- this.el = document.createElement('video');
- this.el.setAttribute('poster', this.posterImage);
- this.el.setAttribute('src', this.rendition.url);
- this.el.setAttribute('id', videoIdProperty);
- this.el.className = Array.isArray(this.classes) ? this.classes.join(' ') : this.classes;
- this.el.classList.add('o-video--videojs');
- this.containerEl.appendChild(this.el);
- this.el.addEventListener('playing', () => pauseOtherVideos(this.el));
- return ensureAllScriptsAreLoaded()
- .then(() => {
- let videoPlayer = videojs(videoIdProperty, {"controls": true,"autoplay": true,"preload": "auto"}).width('100%');
- if(advertising) {
- this.advertising(videoPlayer, videoIdProperty);
- }
- });
- }
-
- getVideoBrand() {
- if(!this.brightcoveData.tags || this.brightcoveData.tags.length === 0) {
- return false;
- } else {
- let filtered = this.brightcoveData.tags.filter(val => val.toLowerCase().indexOf('brand:') !== -1);
- if(filtered.length > 0) {
- try {
- // when we target the value in the ad server, we only want to target actual brand name, so we strip out "brand:" part of the string
- return filtered.pop().substring(6);
- }
- catch (e) {
- return false;
- }
- } else {
- return false;
- }
- }
- }
-
- advertising(player, videoIdProperty) {
- // ad server request call that contains ad server details such as: site(iu), sizes(sz), position(pos), video id(ttid) and branding(brand) if it is available
- // these key values are then used on ad server to target pre roll advertising
- let advertisingUrl = `http://pubads.g.doubleclick.net/gampad/ads?env=vp&gdfp_req=1&impl=s&output=xml_vast2&iu=${this.targeting.site}&sz=${this.targeting.sizes}&unviewed_position_start=1&scp=pos%3D${this.targeting.position}&ttid=${this.targeting.videoId}`;
- if(this.targeting.brand) {
- advertisingUrl += `&brand=${encodeURIComponent(this.targeting.brand)}`;
- }
-
- player.ima({
- id: videoIdProperty,
- adTagUrl: advertisingUrl
- });
- player.ima.requestAds();
- }
-
- addPlaceholder() {
- this.placeholderEl = document.createElement('img');
- this.placeholderEl.setAttribute('src', this.posterImage);
- this.placeholderEl.className = Array.isArray(this.classes) ? this.classes.join(' ') : this.classes;
- this.containerEl.classList.add('o-video--placeholder');
-
- this.containerEl.appendChild(this.placeholderEl);
-
- let titleEl;
- if (this.opts.placeholderTitle) {
- titleEl = document.createElement('div');
- titleEl.className = 'o-video__title';
- titleEl.textContent = this.brightcoveData.name;
- this.containerEl.appendChild(titleEl);
- }
-
- if (this.opts.playButton) {
-
- const playButtonEl = document.createElement('button');
- playButtonEl.className = 'o-video__play-button';
- this.containerEl.appendChild(playButtonEl);
-
- const playIconEl = document.createElement('i');
- playIconEl.className = 'o-video__play-button-icon';
- playButtonEl.appendChild(playIconEl);
-
- playButtonEl.addEventListener('click', () => {
- this.containerEl.removeChild(playButtonEl);
- if (titleEl) {
- this.containerEl.removeChild(titleEl);
- }
- this.removePlaceholder();
- this.addVideo();
- this.el.focus();
- });
- }
- }
-
- removePlaceholder() {
- this.containerEl.classList.remove('o-video--placeholder');
- this.containerEl.removeChild(this.placeholderEl);
- }
-
-}
-
-module.exports = VideoJsPlayer;
diff --git a/src/video.js b/src/video.js
new file mode 100644
index 0000000..f2e9bdd
--- /dev/null
+++ b/src/video.js
@@ -0,0 +1,249 @@
+/* global fetch */
+import crossDomainFetch from 'o-fetch-jsonp';
+import getRendition from './helpers/get-rendition';
+import VideoAds from './ads';
+
+function eventListener(video, ev) {
+ const event = new CustomEvent('oTracking.event', {
+ detail: {
+ action: 'media',
+ advertising: video.opts.advertising,
+ category: 'video',
+ event: ev.type,
+ mediaType: 'video',
+ contentId: video.opts.id,
+ progress: video.getProgress(),
+ },
+ bubbles: true
+ });
+ document.body.dispatchEvent(event);
+};
+
+function addEvents(video, events) {
+ events.forEach(event => {
+ video.videoEl.addEventListener(event, eventListener.bind(this, video));
+ });
+};
+
+// use the image resizing service, if width supplied
+function updatePosterUrl(posterImage, width) {
+ let url = `https://image.webservices.ft.com/v1/images/raw/${encodeURIComponent(posterImage)}?source=o-video`;
+ if (width) {
+ url += `&fit=scale-down&width=${width}`;
+ }
+ return url;
+};
+
+// converts data-o-video attributes to an options object
+function getOptionsFromDataAttributes(attributes) {
+ const opts = {};
+ // Try to get config set declaratively on the element
+ Array.prototype.forEach.call(attributes, (attr) => {
+ if (attr.name.indexOf('data-o-video') === 0) {
+ // Remove the prefix part of the data attribute name
+ const key = attr.name.replace('data-o-video-', '');
+ try {
+ // If it's a JSON, a boolean or a number, we want it stored like that, and not as a string
+ // We also replace all ' with " so JSON strings are parsed correctly
+ opts[key] = JSON.parse(attr.value.replace(/\'/g, '"'));
+ } catch (e) {
+ opts[key] = attr.value;
+ }
+ }
+ });
+
+ return opts;
+}
+
+const defaultOpts = {
+ advertising: false,
+ autorender: true,
+ classes: [],
+ optimumwidth: null,
+ placeholder: false,
+ placeholdertitle: false,
+ playButton: true,
+ data: null
+};
+
+class Video {
+ constructor(el, opts) {
+ this.containerEl = el;
+
+ this.opts = opts || getOptionsFromDataAttributes(this.containerEl.attributes);
+
+ Object.keys(defaultOpts).forEach(optionName => {
+ if (typeof this.opts[optionName] === 'undefined') {
+ this.opts[optionName] = defaultOpts[optionName];
+ }
+ });
+
+ if (typeof this.opts.classes === 'string') {
+ this.opts.classes = this.opts.classes.split(' ');
+ }
+ this.opts.classes.push('o-video__video');
+
+ this.targeting = {
+ site: '/5887/ft.com',
+ position: 'video',
+ sizes: '592x333|400x225',
+ videoId: this.opts.id
+ };
+
+ if (this.opts.advertising) {
+ this.videoAds = new VideoAds(this);
+ }
+
+ this.containerEl.setAttribute('data-o-video-js', '');
+
+ if (this.opts.autorender === true) {
+ this.init();
+ }
+ }
+
+ getData() {
+ const dataPromise = this.opts.data ?
+ Promise.resolve(this.opts.data) :
+ crossDomainFetch(`//next-video.ft.com/api/${this.opts.id}`)
+ .then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw Error('Brightcove responded with a ' + response.status + ' (' + response.statusText + ') for id ' + this.opts.id);
+ }
+ });
+
+
+ return dataPromise.then(data => {
+ this.videoData = data;
+ this.posterImage = updatePosterUrl(data.videoStillURL, this.opts.optimumwidth);
+ this.rendition = getRendition(data.renditions);
+ });
+ }
+
+ renderVideo() {
+ if (this.rendition) {
+ if (this.opts.placeholder) {
+ this.addPlaceholder();
+ } else {
+ this.addVideo();
+ }
+ }
+ }
+
+ init() {
+ let loadAdsLibraryPromise = Promise.resolve();
+ if (this.opts.advertising) {
+ loadAdsLibraryPromise = this.videoAds.loadAdsLibrary();
+ }
+ return loadAdsLibraryPromise
+ .then(() => this.getData())
+ .then(() => this.renderVideo());
+ }
+
+ addVideo() {
+ this.videoEl = document.createElement('video');
+ this.videoEl.setAttribute('controls', true);
+ this.videoEl.setAttribute('poster', this.posterImage);
+ this.videoEl.setAttribute('src', this.rendition && this.rendition.url);
+ this.videoEl.className = Array.isArray(this.opts.classes) ? this.opts.classes.join(' ') : this.opts.classes;
+ this.containerEl.classList.add('o-video--player');
+ this.containerEl.appendChild(this.videoEl);
+
+ addEvents(this, ['play', 'pause', 'ended']);
+ this.videoEl.addEventListener('playing', this.pauseOtherVideos);
+ this.videoEl.addEventListener('suspend', this.clearCurrentlyPlaying);
+ this.videoEl.addEventListener('ended', this.clearCurrentlyPlaying);
+
+ if (this.opts.advertising) {
+ this.videoAds.setUpAds();
+ }
+ }
+
+ addPlaceholder() {
+ this.placeholderEl = document.createElement('img');
+ this.placeholderEl.setAttribute('src', this.posterImage);
+ this.placeholderEl.classList.add('o-video__placeholder');
+ this.containerEl.appendChild(this.placeholderEl);
+
+ let titleEl;
+ if (this.opts.placeholdertitle) {
+ titleEl = document.createElement('div');
+ titleEl.className = 'o-video__title';
+ titleEl.textContent = this.videoData && this.videoData.name;
+ this.containerEl.appendChild(titleEl);
+ }
+
+ if (this.opts.playButton) {
+ const playButtonEl = document.createElement('button');
+ playButtonEl.className = 'o-video__play-button';
+
+ const playButtonTextEl = document.createElement('dd');
+ playButtonTextEl.className = 'o-video__play-button-text';
+ playButtonTextEl.textContent = 'Play video';
+ playButtonEl.appendChild(playButtonTextEl);
+
+ const playIconEl = document.createElement('i');
+ playIconEl.className = 'o-video__play-button-icon';
+ playButtonEl.appendChild(playIconEl);
+
+ this.containerEl.appendChild(playButtonEl);
+
+ playButtonEl.addEventListener('click', () => {
+ this.containerEl.removeChild(playButtonEl);
+ if (titleEl) {
+ this.containerEl.removeChild(titleEl);
+ }
+ this.containerEl.removeChild(this.placeholderEl);
+
+ this.videoEl.style.display = 'block';
+ this.videoEl.play();
+ this.videoEl.focus();
+ });
+ }
+
+ // Adds video soon so ads can start loading
+ this.addVideo();
+ // Hide it so it doesn't flash until the placeholder image loads
+ if (this.videoEl) {
+ this.videoEl.style.display = 'none';
+ }
+ }
+
+ getProgress() {
+ return this.videoEl.duration ? parseInt(100 * this.videoEl.currentTime / this.videoEl.duration, 10) : 0;
+ }
+
+ pauseOtherVideos() {
+ if (this.currentlyPlayingVideo && this.currentlyPlayingVideo !== this.videoEl) {
+ this.currentlyPlayingVideo.pause();
+ }
+
+ this.currentlyPlayingVideo = this.videoEl;
+ }
+
+ clearCurrentlyPlaying() {
+ if (this.currentlyPlayingVideo !== this.videoEl) {
+ this.currentlyPlayingVideo = null;
+ }
+ }
+
+ static init(rootEl, config) {
+ const videos = [];
+ if (!rootEl) {
+ rootEl = document.body;
+ } else if (typeof rootEl === 'string') {
+ rootEl = document.querySelector(rootEl);
+ }
+
+ const videoEls = rootEl.querySelectorAll(':not([data-o-video-js])[data-o-component~="o-video"]');
+
+ for (let i = 0; i < videoEls.length; i++) {
+ videos.push(new Video(videoEls[i], config));
+ }
+
+ return videos;
+ }
+}
+
+export default Video;
diff --git a/test/ads.test.js b/test/ads.test.js
new file mode 100644
index 0000000..91fa583
--- /dev/null
+++ b/test/ads.test.js
@@ -0,0 +1,116 @@
+/* global describe, it, beforeEach, afterEach, google */
+const Ads = require('./../src/ads');
+const sinon = require('sinon/pkg/sinon');
+
+describe('Ads', () => {
+
+ let ads;
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ const videoEl = document.createElement('video');
+ containerEl.appendChild(videoEl);
+ document.body.appendChild(containerEl);
+ const video = {
+ containerEl,
+ videoEl,
+ targeting: {}
+ };
+ ads = new Ads(video);
+ });
+
+ afterEach(() => {
+ ads = null;
+ document.body.removeChild(containerEl);
+ });
+
+ it('should have all ad event handlers', () => {
+ Ads.prototype.adsManagerLoadedHandler.should.be.a('function');
+ Ads.prototype.adErrorHandler.should.be.a('function');
+ Ads.prototype.adEventHandler.should.be.a('function');
+ Ads.prototype.contentPauseRequestHandler.should.be.a('function');
+ Ads.prototype.contentResumeRequestHandler.should.be.a('function');
+ });
+
+ it('should add the video advertising script if the configuration parameter is passed', () => {
+ return ads.loadAdsLibrary()
+ .then(() => {
+ document.querySelector('[src="//imasdk.googleapis.com/js/sdkloader/ima3.js"]').should.exist;
+ });
+ });
+
+ describe('#setUpAds', () => {
+ it('should set up ads', () => {
+ ads.setUpAds();
+
+ ads.adContainerEl.should.be.an('htmldivelement');
+ ads.adDisplayContainer.should.be.an('object');
+ ads.adsLoader.should.be.an('object');
+ });
+
+ it('should set up event handlers', () => {
+ const realAddEventListener = google.ima.AdsLoader.prototype.addEventListener;
+ const addEventListenerSpy = sinon.spy();
+ google.ima.AdsLoader.prototype.addEventListener = addEventListenerSpy;
+
+ ads.setUpAds();
+
+ google.ima.AdsLoader.prototype.addEventListener.calledWith(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, ads.adsManagerLoadedHandler).should.equal(true);
+ google.ima.AdsLoader.prototype.addEventListener.calledWith(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, ads.adsManagerLoadedHandler).should.equal(true);
+
+ google.ima.AdsLoader.prototype.addEventListener = realAddEventListener;
+ });
+ });
+
+ describe('#adsManagerLoadedHandler', () => {
+ it('should set up adsManager', () => {
+ const adsManagerLoadedEvent = {
+ getAdsManager: sinon.stub().returns({
+ addEventListener: () => {}
+ })
+ };
+
+ ads.video.videoEl = {
+ played: 0,
+ addEventListener: () => {}
+ };
+
+ ads.adsManagerLoadedHandler(adsManagerLoadedEvent);
+ ads.adsManager.should.be.an('object');
+ adsManagerLoadedEvent.getAdsManager.called.should.equal(true);
+ });
+ });
+
+ describe('#playAdEventHandler', () => {
+
+ const realAdsManagerLoadedHandler = Ads.prototype.adsManagerLoadedHandler;
+ const adsManagerLoadedHandlerStub = sinon.stub();
+ const adsManagerStub = sinon.stub();
+
+ beforeEach(() => {
+ Ads.prototype.adsManagerLoadedHandler = adsManagerLoadedHandlerStub;
+ ads.setUpAds();
+ ads.adsManager = adsManagerStub;
+ });
+
+ afterEach(() => {
+ ads.adsManager = undefined;
+ Ads.prototype.adsManagerLoadedHandler = realAdsManagerLoadedHandler;
+ });
+
+ it('should play ad', () => {
+ ads.playAdEventHandler();
+ ads.adContainerEl.classList.toString().should.contain('o-video__ad');
+ });
+ });
+
+ describe('#getVideoBrand', () => {
+ it('should get the brand for targeting', () => {
+ ads.video.videoData = {
+ tags: ['brand:Authers Note']
+ };
+ ads.getVideoBrand().should.equal('Authers Note');
+ });
+ });
+});
diff --git a/test/libs/get-appropriate-rendition.test.js b/test/helpers/get-appropriate-rendition.test.js
similarity index 52%
rename from test/libs/get-appropriate-rendition.test.js
rename to test/helpers/get-appropriate-rendition.test.js
index fcd31b4..986aa91 100644
--- a/test/libs/get-appropriate-rendition.test.js
+++ b/test/helpers/get-appropriate-rendition.test.js
@@ -1,27 +1,27 @@
/* global describe, it */
-const getAppropriateRendition = require('../../src/libs/get-appropriate-rendition');
-const renditions = require('../fixtures/brightcove.json').renditions;
+const getRendition = require('./../../src/helpers/get-rendition');
+const renditions = require('./../fixtures/brightcove.json').renditions;
describe('Get Appropriate Renditions', () => {
const supportedFormats = ['h264'];
it('should exist', () => {
- getAppropriateRendition.should.exist;
+ getRendition.should.exist;
});
it('should get largest if no width supplied', () => {
- getAppropriateRendition(renditions, { supportedFormats: supportedFormats })
+ getRendition(renditions, { supportedFormats: supportedFormats })
.should.have.property('id', 4085577922001);
});
it('should get rendition of at least the width supplied', () => {
- getAppropriateRendition(renditions, { supportedFormats: supportedFormats, width: 410 })
+ getRendition(renditions, { supportedFormats: supportedFormats, width: 410 })
.should.have.property('id', 4085577902001);
});
it('should get smallest rendition if width is small', () => {
- getAppropriateRendition(renditions, { supportedFormats: supportedFormats, width: 390 })
+ getRendition(renditions, { supportedFormats: supportedFormats, width: 390 })
.should.have.property('id', 4085577899001);
});
diff --git a/test/libs/supported-formats.test.js b/test/helpers/supported-formats.test.js
similarity index 64%
rename from test/libs/supported-formats.test.js
rename to test/helpers/supported-formats.test.js
index 230dfc2..acbe5e5 100644
--- a/test/libs/supported-formats.test.js
+++ b/test/helpers/supported-formats.test.js
@@ -1,5 +1,5 @@
/* global describe, it */
-const supportedFormats = require('../../src/libs/supported-formats');
+const supportedFormats = require('./../../src/helpers/supported-formats');
describe('Supported Formats', () => {
diff --git a/test/main.test.js b/test/main.test.js
deleted file mode 100644
index 2aac5b0..0000000
--- a/test/main.test.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/* global describe, it, beforeEach, afterEach */
-const sinon = require('sinon/pkg/sinon');
-const main = require('../main.js');
-const Brightcove = require('../src/models/brightcove');
-const brightcoveResponse = require('./fixtures/brightcove.json');
-
-describe('Main', () => {
-
- let containerEl;
- let fetchStub;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.setAttribute('data-o-component', 'o-video');
- containerEl.setAttribute('data-o-video-id', '4084879507001');
- containerEl.setAttribute('data-o-video-source', 'Brightcove');
- document.body.appendChild(containerEl);
- fetchStub = sinon.stub(window, 'fetch');
- const res = new window.Response(JSON.stringify(brightcoveResponse), {
- status: 200,
- headers: {
- 'Content-type': 'application/json'
- }
- });
- fetchStub.returns(Promise.resolve(res));
- });
-
- afterEach(() => {
- document.body.removeChild(containerEl);
- fetchStub.restore();
- });
-
- it('should exits', () => {
- main.should.exist;
- });
-
- it('should have an init property', () => {
- main.should.have.property('init');
- });
-
- it('should create a Video object and element', () => {
- return main.init().then(videos => {
- videos.should.have.length(1);
- videos[0].should.be.an.instanceOf(Brightcove);
- containerEl.children[0].should.eql(videos[0].el);
- });
- });
-
- it('should create Video objects only once', () => {
- return main.init().then(() => {
- main.init().then(videos => {
- videos.should.be.empty;
- });
- });
- });
-
- it('should allow setting the selector Video objects only once', () => {
- const className = 'js-video';
-
- return main.init({ selector: '.' + className }).then(videos => {
- containerEl.className = className;
- videos.should.be.empty;
- return main.init({ selector: '.' + className }).then(videos => {
- videos.should.have.length(1);
- });
- });
- });
-
- it('should allow setting options through attribute', () => {
- containerEl.setAttribute('data-o-video-opts-optimum-width', 300);
- containerEl.setAttribute('data-o-video-opts-placeholder', true);
- containerEl.setAttribute('data-o-video-opts-classes', 'a-class another-class');
-
- return main.init().then(videos => {
- videos.should.have.length(1);
- const placeholderEl = videos[0].containerEl.querySelector('img');
- placeholderEl.className.should.equal('o-video__placeholder');
- placeholderEl.getAttribute('src').should.contain('width=300');
- });
- });
-
-});
diff --git a/test/models/brightcove.test.js b/test/models/brightcove.test.js
deleted file mode 100644
index 1380acd..0000000
--- a/test/models/brightcove.test.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/* global describe, it, beforeEach, afterEach, sinon, expect */
-const Brightcove = require('../../src/models/brightcove');
-const brightcoveResponse = require('../fixtures/brightcove.json');
-
-describe('Brightcove', () => {
-
- let containerEl;
- let fetchStub;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.setAttribute('data-o-video-id', '4084879507001');
- document.body.appendChild(containerEl);
- fetchStub = sinon.stub(window, 'fetch');
- const res = new window.Response(JSON.stringify(brightcoveResponse), {
- status: 200,
- headers: {
- 'Content-type': 'application/json'
- }
- });
- fetchStub.returns(Promise.resolve(res));
- });
-
- afterEach(() => {
- document.body.removeChild(containerEl);
- fetchStub.restore();
- });
-
- it('should exist', () => {
- Brightcove.should.exist;
- });
-
- it('should be able to instantiate', () => {
- const brightcove = new Brightcove(containerEl);
- brightcove.should.exist;
- });
-
- it('should return a Promise on `init`', () => {
- const brightcove = new Brightcove(containerEl);
- brightcove.init().should.be.an.instanceOf(Promise);
- });
-
- it('should return the Brightcove instance on `init`', () => {
- const brightcove = new Brightcove(containerEl);
- brightcove.init().should.eventually.equal(brightcove);
- });
-
- it('should create a video element on `init`', () => {
- const brightcove = new Brightcove(containerEl);
- return brightcove
- .init()
- .then(() => {
- const videoEl = containerEl.querySelector('video');
- videoEl.getAttribute('poster').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- videoEl.getAttribute('src').should.equal(
- 'http://brightcove.vo.llnwd.net/v1/uds/pd/47628783001/201502/3842/47628783001_4085577922001_A-hated-rally.mp4'
- );
- });
- });
-
- it('should throw error if can\'t init', () => {
- // bad response instead
- const badRes = new window.Response(null, {
- status: 404,
- statusText: 'Not Found'
- });
- fetchStub.returns(Promise.resolve(badRes));
- const brightcove = new Brightcove(containerEl);
- return brightcove.init().should.be.rejectedWith('Brightcove responded with a 404 (Not Found) for id 4084879507001');
- });
-
- it('should return the progress as a percentage', () => {
- const brightcove = new Brightcove(containerEl);
- return brightcove
- .init()
- .then(() => {
- // TODO: mock different values
- brightcove.getProgress().should.equal(0);
- });
- });
-
- it('should be able to create as a placeholder', () => {
- const brightcove = new Brightcove(containerEl, { placeholder: true });
- return brightcove
- .init()
- .then(() => {
- const placholderEl = containerEl.querySelector('img');
- placholderEl.getAttribute('src').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- containerEl.querySelector('.o-video__play-button').should.exist;
- });
- });
-
- it('should be able to create a placeholder with a title', () => {
- const brightcove = new Brightcove(containerEl, { placeholder: true, placeholderTitle: true });
- return brightcove
- .init()
- .then(() => {
- const placholderEl = containerEl.querySelector('img');
- placholderEl.getAttribute('src').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- containerEl.querySelector('.o-video__play-button').should.exist;
- containerEl.querySelector('.o-video__title').textContent.should.equal('A hated rally');
- });
- });
-
- it('should be able to suppress placeholder play button', () => {
- const brightcove = new Brightcove(containerEl, { placeholder: true, playButton:false });
- return brightcove
- .init()
- .then(() => {
- expect(containerEl.querySelector('.o-video__play-button')).to.be.null;
- });
- });
-
- it('should send poster through image service if optimumWidth defined', () => {
- const brightcove = new Brightcove(containerEl, { optimumWidth: 300 });
- return brightcove
- .init()
- .then(() => {
- containerEl.querySelector('video').getAttribute('poster').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video&fit=scale-down&width=300'
- );
- });
- });
-
- it('should add supplied classes to element', () => {
- const brightcove = new Brightcove(containerEl, { classes: ['class-one', 'class-two'] });
- return brightcove
- .init()
- .then(() => {
- containerEl.querySelector('video').className.should.equal('class-one class-two o-video__video');
- });
- });
-
- it('should not fetch from brightcove if full data provided in opts', () => {
- const brightcove = new Brightcove(containerEl, { data: {
- prop: 'val',
- videoStillURL: 'abc',
- renditions: []
- }});
- return brightcove
- .getData()
- .then(() => {
- brightcove.brightcoveData.prop.should.equal('val');
- });
- });
-
-});
diff --git a/test/models/video-factory.test.js b/test/models/video-factory.test.js
deleted file mode 100644
index 36565c2..0000000
--- a/test/models/video-factory.test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* global describe, it, beforeEach, afterEach */
-const videoFactory = require('../../src/models/video-factory');
-const Brightcove = require('../../src/models/brightcove');
-const Video = require('../../src/models/video');
-const VideoJS = require('../../src/models/videojs');
-
-describe('Video Factory', () => {
-
- let containerEl;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.setAttribute('data-o-video-id', '1234567890');
- document.body.appendChild(containerEl);
- });
-
- afterEach(() => {
- document.body.removeChild(containerEl);
- });
-
- it('should exist', () => {
- videoFactory.should.exist;
- });
-
- it('should create a Brightcove object if source is \'brightcove\'', () => {
- containerEl.setAttribute('data-o-video-source', 'brightcove');
- videoFactory(containerEl).should.be.a.instanceof(Brightcove);
- });
-
- it('should create a VideoJS object if player is \'videojs\'', () => {
- containerEl.setAttribute('data-o-video-source', 'brightcove');
- containerEl.setAttribute('data-o-video-player', 'videojs');
- videoFactory(containerEl).should.be.an.instanceOf(VideoJS);
- });
-
- it('should create a standard Video object if unknown source', () => {
- containerEl.setAttribute('data-o-video-source', 'other');
- videoFactory(containerEl).should.be.an.instanceOf(Video);
- });
-
-});
diff --git a/test/models/video.test.js b/test/models/video.test.js
deleted file mode 100644
index c9d897e..0000000
--- a/test/models/video.test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* global describe, it, beforeEach, afterEach */
-const Video = require('../../src/models/video');
-
-describe('Video', () => {
-
- let containerEl;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.setAttribute('data-o-video-id', '1234567890');
- document.body.appendChild(containerEl);
- });
-
- afterEach(() => {
- document.body.removeChild(containerEl);
- });
-
- it('should exist', () => {
- Video.should.exist;
- });
-
- it('should be able to instantiate', () => {
- const video = new Video(containerEl);
- video.should.exist;
- });
-
- it('should an a `data-o-video-js` attribute', () => {
- new Video(containerEl);
- containerEl.getAttribute('data-o-video-js').should.exists;
- });
-
- it('should return a Promise on init', () => {
- const video = new Video(containerEl);
- video.init().should.be.an.instanceOf(Promise);
- });
-
-});
diff --git a/test/models/videojs.test.js b/test/models/videojs.test.js
deleted file mode 100644
index 987bf06..0000000
--- a/test/models/videojs.test.js
+++ /dev/null
@@ -1,183 +0,0 @@
-/* global describe, it, beforeEach, afterEach, sinon, expect */
-let VideoJS = require('../../src/models/videojs');
-const oVideo = require('../../main');
-const brightcoveResponse = require('../fixtures/brightcove.json');
-
-const checkGoogleVideoSdkLoaded = function() {
- return [].slice.call(document.getElementsByTagName('script'))
- .some(function(s) {
- if(s.src.indexOf('//imasdk.googleapis.com/js/sdkloader/ima3.js') > -1) {
- return true;
- } else {
- return false;
- }
- });
-};
-
-describe('VideoJS', () => {
-
- let containerEl;
- let fetchStub;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.setAttribute('data-o-video-id', '4084879507001');
- document.body.appendChild(containerEl);
- fetchStub = sinon.stub(window, 'fetch');
- const res = new window.Response(JSON.stringify(brightcoveResponse), {
- status: 200,
- headers: {
- 'Content-type': 'application/json'
- }
- });
- fetchStub.returns(Promise.resolve(res));
- VideoJS = require('../../src/models/videojs');
- });
-
- afterEach(() => {
- document.body.removeChild(containerEl);
- fetchStub.restore();
- VideoJS = null;
- });
-
- it('should exist', () => {
- VideoJS.should.exist;
- });
-
- it('should be able to instantiate', () => {
- const videojsPlayer = new VideoJS(containerEl);
- videojsPlayer.should.exist;
- });
-
- it('should return a Promise on `init`', () => {
- const videojsPlayer = new VideoJS(containerEl);
- videojsPlayer.init().should.be.an.instanceOf(Promise);
- });
-
- it('should return the VideoJS instance on `init`', () => {
- const videojsPlayer = new VideoJS(containerEl);
- videojsPlayer.init().should.eventually.equal(videojsPlayer);
- });
-
- it('should create a video element on `init`', () => {
- const videojsPlayer = new VideoJS(containerEl);
- return videojsPlayer
- .init()
- .then(() => {
- const videoEl = containerEl.querySelector('video');
- videoEl.getAttribute('poster').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- videoEl.getAttribute('src').should.equal(
- 'http://brightcove.vo.llnwd.net/v1/uds/pd/47628783001/201502/3842/47628783001_4085577922001_A-hated-rally.mp4'
- );
- const checkSdkIsLoaded = checkGoogleVideoSdkLoaded();
- checkSdkIsLoaded.should.equal(false);
- });
- });
-
- it('should throw error if can\'t init', () => {
- // bad response instead
- const badRes = new window.Response(null, {
- status: 404,
- statusText: 'Not Found'
- });
- fetchStub.returns(Promise.resolve(badRes));
- const videojsPlayer = new VideoJS(containerEl);
- return videojsPlayer.init().should.be.rejectedWith('Brightcove responded with a 404 (Not Found) for id 4084879507001');
- });
-
- it('should be able to create as a placeholder', () => {
- const videojsPlayer = new VideoJS(containerEl, { placeholder: true });
- return videojsPlayer
- .init()
- .then(() => {
- const placholderEl = containerEl.querySelector('img');
- placholderEl.getAttribute('src').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- containerEl.querySelector('.o-video__play-button').should.exist;
- });
- });
-
- it('should be able to create a placeholder with a title', () => {
- const videojsPlayer = new VideoJS(containerEl, { placeholder: true, placeholderTitle: true });
- return videojsPlayer
- .init()
- .then(() => {
- const placholderEl = containerEl.querySelector('img');
- placholderEl.getAttribute('src').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video'
- );
- containerEl.querySelector('.o-video__play-button').should.exist;
- containerEl.querySelector('.o-video__title').textContent.should.equal('A hated rally');
- });
- });
-
- it('should be able to suppress placeholder play button', () => {
- const videojsPlayer = new VideoJS(containerEl, { placeholder: true, playButton:false });
- return videojsPlayer
- .init()
- .then(() => {
- expect(containerEl.querySelector('.o-video__play-button')).to.be.null;
- });
- });
-
- it('should send poster through image service if optimumWidth defined', () => {
- const videojsPlayer = new VideoJS(containerEl, { optimumWidth: 300 });
- return videojsPlayer
- .init()
- .then(() => {
- containerEl.querySelector('video').getAttribute('poster').should.equal(
- 'https://image.webservices.ft.com/v1/images/raw/' +
- 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
- '?source=o-video&fit=scale-down&width=300'
- );
- });
- });
-
- it('should add supplied classes to video tag', () => {
- const videojsPlayer = new VideoJS(containerEl, { classes: ['class-one', 'class-two'] });
- return videojsPlayer
- .init()
- .then(() => {
- const videoElement = containerEl.querySelector('video');
- videoElement.className.should.contain('class-one');
- videoElement.className.should.contain('class-two');
- });
- });
-
- it('should not fetch from brightcove if full data provided in opts', () => {
- const videojsPlayer = new VideoJS(containerEl, { data: {
- prop: 'val',
- videoStillURL: 'abc',
- renditions: []
- }});
- return videojsPlayer
- .getData()
- .then(() => {
- videojsPlayer.brightcoveData.prop.should.equal('val');
- });
- });
-
- it('should add the video advertising script if the configuration parameter is passed', (done) => {
- const videojsPlayer = new VideoJS(containerEl, {advertising: true});
- return oVideo._loadAdsLibrary().then(() => {
- videojsPlayer
- .init()
- .then(() => {
- const checkSdkIsLoaded = checkGoogleVideoSdkLoaded();
- checkSdkIsLoaded.should.equal(true);
- videojsPlayer.targeting.brand.should.equal('Authers Note');
- done();
- });
- });
- });
-
-});
diff --git a/test/video.test.js b/test/video.test.js
new file mode 100644
index 0000000..266dc86
--- /dev/null
+++ b/test/video.test.js
@@ -0,0 +1,259 @@
+/* global describe, it, beforeEach, afterEach, before, after, should */
+const Video = require('./../src/video');
+const brightcoveResponse = require('./fixtures/brightcove.json');
+const sinon = require('sinon/pkg/sinon');
+
+describe('Video', () => {
+
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ containerEl.setAttribute('data-o-component', 'o-video');
+ containerEl.setAttribute('data-o-video-id', '4084879507001');
+ containerEl.setAttribute('data-o-video-source', 'Brightcove');
+ document.body.appendChild(containerEl);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(containerEl);
+ });
+
+ describe('constructor', () => {
+ it('should be able to instantiate', () => {
+ const video = new Video(containerEl);
+ video.should.be.an.instanceOf(Video);
+
+ video.opts.should.exist;
+ video.opts.id.should.eql(4084879507001);
+
+ video.targeting.should.exist;
+ video.targeting.site.should.eql('/5887/ft.com');
+ video.targeting.position.should.eql('video');
+ video.targeting.sizes.should.eql('592x333|400x225');
+ video.targeting.videoId.should.eql(4084879507001);
+
+ video.containerEl.should.eql(containerEl);
+ video.containerEl.hasAttribute('data-o-video-js').should.be.true;
+ });
+
+ it('should set default options', () => {
+ const video = new Video(containerEl);
+
+ video.opts.advertising.should.eql(false);
+ video.opts.autorender.should.eql(true);
+ video.opts.classes.should.be.an.instanceOf(Array);
+ video.opts.classes.should.contain('o-video__video');
+ should.equal(video.opts.optimumwidth, null);
+ video.opts.placeholder.should.eql(false);
+ video.opts.playButton.should.eql(true);
+ should.equal(video.opts.data, null);
+ });
+
+ it('should allow setting options through attribute', () => {
+ containerEl.setAttribute('data-o-video-optimumwidth', 300);
+ containerEl.setAttribute('data-o-video-placeholder', true);
+ containerEl.setAttribute('data-o-video-classes', 'a-class another-class');
+
+ const video = new Video(containerEl);
+ video.opts.optimumwidth.should.eql(300);
+ video.opts.placeholder.should.eql(true);
+ video.opts.classes.should.contain('a-class');
+ video.opts.classes.should.contain('another-class');
+ });
+ });
+
+ describe('#init', () => {
+ it('should return an array of video instances', () => {
+ const videos = Video.init();
+ videos.length.should.eql(1);
+ videos[0].should.be.an.instanceOf(Video);
+ });
+
+ it('should create Video objects only once', () => {
+ const videos = Video.init();
+ videos.length.should.eql(1);
+ const videos2 = Video.init();
+ videos2.length.should.eql(0);
+ });
+ });
+
+ describe('#addVideo', () => {
+ it('should add a video element', () => {
+ const video = new Video(containerEl);
+ video.rendition = {
+ url: 'http://url.mock'
+ };
+ video.posterImage = 'mockimage';
+ video.addVideo();
+
+ video.videoEl.should.be.an.instanceOf(HTMLElement);
+ video.videoEl.parentElement.should.equal(containerEl);
+ video.videoEl.getAttribute('poster').should.equal('mockimage');
+ video.videoEl.getAttribute('src').should.equal('http://url.mock');
+ video.videoEl.getAttribute('controls').should.equal('true');
+ });
+
+ it('should add supplied classes to element', () => {
+ const video = new Video(containerEl, { classes: ['class-one', 'class-two'] });
+ video.addVideo();
+ video.videoEl.className.should.equal('class-one class-two o-video__video');
+ });
+
+ it('should set event handlers', () => {
+ const video = new Video(containerEl);
+ const realAddEventListener = Element.prototype.addEventListener;
+ const addEventListenerSpy = sinon.spy();
+ Element.prototype.addEventListener = addEventListenerSpy;
+
+ video.addVideo();
+ addEventListenerSpy.callCount.should.equal(6);
+ addEventListenerSpy.alwaysCalledOn(video.videoEl).should.equal(true);
+ addEventListenerSpy.calledWith('playing', video.pauseOtherVideos);
+ addEventListenerSpy.calledWith('suspend', video.clearCurrentlyPlaying);
+ addEventListenerSpy.calledWith('ended', video.clearCurrentlyPlaying);
+ addEventListenerSpy.calledWith('play');
+ addEventListenerSpy.calledWith('pause');
+ addEventListenerSpy.calledWith('ended');
+
+ Element.prototype.addEventListener = realAddEventListener;
+ });
+ });
+
+ describe('#addPlaceholder', () => {
+
+ const realAddVideo = Video.prototype.addVideo;
+ let addVideoSpy = sinon.spy();
+
+ beforeEach(() => {
+ Video.prototype.addVideo = addVideoSpy;
+ });
+
+ afterEach(() => {
+ Video.prototype.addVideo = realAddVideo;
+ });
+
+ it('should be able to create as a placeholder', () => {
+ const video = new Video(containerEl, { placeholder: true });
+ video.posterImage = 'mockimage';
+ video.addPlaceholder();
+
+ video.placeholderEl.should.be.an.instanceOf(HTMLElement);
+ video.placeholderEl.parentElement.should.equal(containerEl);
+ video.placeholderEl.classList.contains('o-video__placeholder').should.equal(true);
+ video.placeholderEl.getAttribute('src').should.equal('mockimage');
+ containerEl.querySelector('.o-video__play-button').should.exist;
+
+ Video.prototype.addVideo.called.should.equal(true);
+ });
+
+ it('should be able to create a placeholder with a title', () => {
+ const video = new Video(containerEl, { placeholder: true, placeholdertitle: true });
+ video.videoData = { name: 'A hated rally' };
+ video.addPlaceholder();
+ const titleEl = containerEl.querySelector('.o-video__title');
+
+ titleEl.should.exist;
+ titleEl.textContent.should.equal('A hated rally');
+ });
+
+ it('should add a play button', () => {
+ const realAddEventListener = Element.prototype.addEventListener;
+ const addEventListenerSpy = sinon.spy();
+ Element.prototype.addEventListener = addEventListenerSpy;
+
+ const video = new Video(containerEl, { placeholder: true, placeholdertitle: true });
+ video.addPlaceholder();
+ const playButtonEl = containerEl.querySelector('.o-video__play-button');
+ const playButtonTextEl = playButtonEl.querySelector('.o-video__play-button-text');
+ const playIconEl = playButtonEl.querySelector('.o-video__play-button-icon');
+
+ playButtonEl.should.exist;
+ playButtonTextEl.should.exist;
+ playIconEl.should.exist;
+ addEventListenerSpy.called.should.equal(true);
+ addEventListenerSpy.calledWith('click').should.equal(true);
+ addEventListenerSpy.calledOn(playButtonEl);
+
+ Element.prototype.addEventListener = realAddEventListener;
+ });
+
+ it('should be able to suppress placeholder play button', () => {
+ new Video(containerEl, { placeholder: true, playButton: false });
+
+ should.equal(containerEl.querySelector('.o-video__play-button'), null);
+ });
+ });
+
+ describe('#getProgress', () => {
+ let video;
+
+ beforeEach(() => {
+ video = new Video(containerEl);
+ video.videoEl = {};
+ });
+
+ afterEach(() => {
+ video = undefined;
+ });
+
+ it('should return 0 if duration is not set', () => {
+ video.getProgress().should.equal(0);
+ });
+
+ it('should return the progress of the video as a percentage', () => {
+ video.videoEl.duration = 200;
+ video.videoEl.currentTime = 50;
+ video.getProgress().should.equal(25);
+ });
+
+ });
+
+ describe('#getData', () => {
+
+ let fetchStub;
+
+ before(() => {
+ fetchStub = sinon.stub(window, 'fetch');
+ const res = new window.Response(JSON.stringify(brightcoveResponse), {
+ status: 200,
+ headers: {
+ 'Content-type': 'application/json'
+ }
+ });
+ fetchStub.returns(Promise.resolve(res));
+ });
+
+ after(() => {
+ fetchStub.restore();
+ });
+
+ it('should send poster through image service if optimumwidth defined', () => {
+ containerEl.setAttribute('data-o-video-optimumwidth', '300');
+ const video = new Video(containerEl);
+ return video.getData()
+ .then(() => {
+ video.posterImage.should.equal(
+ 'https://image.webservices.ft.com/v1/images/raw/' +
+ 'https%3A%2F%2Fbcsecure01-a.akamaihd.net%2F13%2F47628783001%2F201502%2F2470%2F47628783001_4085962850001_MAS-VIDEO-AuthersNote-stock-market.jpg%3FpubId%3D47628783001' +
+ '?source=o-video&fit=scale-down&width=300'
+ );
+ });
+ });
+
+ it('should not fetch from video if full data provided in opts', () => {
+ const video = new Video(containerEl, { data: {
+ prop: 'val',
+ videoStillUrl: 'abc',
+ renditions: []
+ }});
+ return video
+ .getData()
+ .then(() => {
+ video.videoData.prop.should.equal('val');
+ video.videoData.videoStillUrl.should.equal('abc');
+ video.videoData.renditions.length.should.equal(0);
+ });
+ });
+ });
+});