Skip to content

Commit

Permalink
Merge branch 'master' into open-source-time
Browse files Browse the repository at this point in the history
  • Loading branch information
banderson623 committed Apr 1, 2019
2 parents fa4acfb + c9e0d32 commit ac5455d
Show file tree
Hide file tree
Showing 37 changed files with 990 additions and 199 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ yarn-error.log
dist/*.hot-update.*
stats.json
junit.xml
coverage
test/cypress/screenshots/*
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## v1.2.1 (March 19, 2019)

- Support for autplay and mute options in applications functions

## v1.2 (March 16, 2019)

- Support for all Tracking directives of the form: `Tracking event="progress"`

## v1.1 (March 3, 2019)

- Preparing for open sourcing, adding documentation
- Adds Wrapper/VASTAdTagURI support, `async` in more loading paths
- Moves XML Node Value parsing to it's own library

## v1.0

- Basic Vast support
- Applications for `<video>` element and `videojs`
76 changes: 52 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,76 @@

This is an opinionated and minimal Concert Vast parser. It consumes simple InLine video VAST responses and provides a nice wrapper to interact with the XML response in a Concert-opinionated way.

### Using this library
## Vast Elements Supported

The Vast Libary offers high and low level interacition with the Vast format. The following example is the simplest case to implement preroll on a `<video>` element.
```
┌─────────────────┐
│ │
┌──────▶│ Video │
│ │ │
│ └─────────────────┘
┌─────────────────┐ │ ┌─────────────────┐
│ Inline Vast │ │ │ │
│ Document │──────┼──────▶│ Clickthrough │
│ │ │ │ │
└─▲───────────────┘ │ └─────────────────┘
: ; │
╲ ╱ │ ┌─────────────────┐
`. ,' │ │ │
`─────' └──────▶│ Impressions │
Wrapper Url │ │
Following └─────────────────┘
Progress, Errors
and Impressions
```

### Concert Vast's Applications for Preroll

The Concert Vast libary offers a high interaction with Vast tags.

As of version 1.0 there are currently two built in Applications:

- **`<video>`** element support via `vast.applyToVideoElementAsPreroll(document.querySelector('video'))`
- **[`videojs`](https://videojs.com/)** support using `applyToVideoJsAsPreroll()`

The following example is the simplest case to implement preroll on a `<video>` element.

```html
<video controls>
<video>
<source src="YOUR GREAT VIDEO URL HERE" type="video/mp4" />
</video>
<script>
const videoElement = document.querySelector('video');
const vast = new ConcertVast();
vast.loadRemoteVast('VAST URL HERE').then(e => {
vast.applyToVideoElementAsPreroll(videoElement);
});
const videoElement = document.querySelector('video')
const vast = new ConcertVast()
vast.loadRemoteVast('VAST URL HERE').then({
vast.applyToVideoElementAsPreroll(videoElement)
}).catch(error => {
// handle any errors here
})
</script>
```

It's also possible you don't want the preroll behavior, and you can use:
It's also possible you don't want to load the Vast videos as the main video, in which case the following method is available.

```js
vast.applyToVideoElement(videoElement);
```

#### Doing it on your own
For a full demo of this functionality see the examples found in /test/assets/.

### Full API for Granular Control

Here is a sample of a portion of the public API that is exposed in the ConcertVast Library _(more documentation coming)_

Here is a sample of the public API that is exposed in the ConcertVast Library _(more documentation coming)_
_Note: it is important to remember that all network interactions should anticipate delays and failures. To that end all network-possible methods are `async`-based and maybe throw errors._

```js
// Find the video element on the page
const videoElement = document.querySelector('video');

// Instantiate a new ConcertVast object
const cv = new ConcertVast();
const vast = new ConcertVast();

// Load the VAST URL, this is async so you can use
// await or .then() to delay execution until the vast
Expand All @@ -43,7 +80,7 @@ const cv = new ConcertVast();
// Optionally a timeout parameter (in ms) can be passed in to specify
// how long to wait for a vast response
try {
await cv.loadRemoteVast(url, { timeout: 10000 });
await vast.loadRemoteVast(url, { timeout: 10000 });
} catch (error) {
// if this raises an error, it is for the following reasons:
// - there was a network error (VastNetworkError)
Expand All @@ -55,7 +92,7 @@ try {
// - browser code support,
// - bitrate (from Vast request) and
// - player size)
const bestVastVideo = cv.bestVideo({
const bestVastVideo = vast.bestVideo({
height: videoElement.clientHeight,
width: videoElement.clientWidth,
});
Expand All @@ -68,17 +105,14 @@ const vidSource = document.createElement('source');
vidSource.setAttribute('src', bestVastVideo.url());
vidSource.setAttribute('type', bestVastVideo.mimeType());
videoElement.appendChild(vidSource);
// Need to call load if you change the video source
videoElement.load();

// Or if using videojs
const player = videoJs(videoElement);
player.src([{ type: bestVastVideo.mimeType(), src: bestVastVideo.url() }]);
```

### Documented Functionality

### Clone it and Run it
### Developing and Contributing

- Clone this repo
- Run `yarn install`
Expand All @@ -88,12 +122,6 @@ player.src([{ type: bestVastVideo.mimeType(), src: bestVastVideo.url() }]);
- Run `yarn cypress-open` to run cypress in visual mode
- Run `yarn build` to build the final js

### Remaining Work

1. ~~Test this with the HymnalAd SDK Video player~~
1. ~~Design error handling~~
1. ~~Open source it 🙏~~

### Contributing

- Make a branch
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ module.exports = {
reporters: ['default', 'jest-junit'],
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['<rootDir>/build/', '<rootDir>/node_modules/', '<rootDir>/test/cypress/'],
coveragePathIgnorePatterns: ['/node_modules/', '/test/cypress/', '/src/lib/applications/'],
coverageReporters: ['text-summary', 'lcov'],
};
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "concert-vast",
"version": "1.0.0",
"version": "1.2.1",
"description": "Simple Vast Parsing for Concert Video Ads",
"main": "src/index.js",
"author": "Vox Media",
Expand Down Expand Up @@ -29,11 +29,10 @@
"jest-junit": "^6.2.1",
"prettier": "1.16.4",
"pretty-quick": "^1.10.0",
"webpack": "^4.29.3",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.0"
},
"dependencies": {
"webpack": "^4.29.3"
"webpack-dev-server": "^3.2.0",
"xhr-mock": "^2.4.1"
},
"husky": {
"hooks": {
Expand Down
35 changes: 29 additions & 6 deletions src/lib/applications/video_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,48 @@ const VAST_LOADED_CLASS = 'vast-running';
const VAST_PLAYING_CLASS = 'vast-playing';
const VAST_DELAYED_ATTRIBUTE = 'vast-delayed-src';

const DEFAULT_OPTIONS = {
autoplay: false,
muted: true,
restoreOriginalVideoOnComplete: true,
};

export default class VideoElement {
constructor({ vast, videoElement }) {
this.vast = vast;
this.videoElement = videoElement;
this.previousVolume = this.videoElement.volume;
this.quartileSupport = new QuartileSupport();
this._vastPresented = null;

this.restoreVideoPlayer = false;
this.autoplay = false;
this.muted = true;
}

applyAsPreroll() {
applyAsPreroll(options = {}) {
options = Object.assign({}, DEFAULT_OPTIONS, options);
this.autoplay = options.autoplay;
this.muted = options.muted;
this.restoreVideoPlayer = options.restoreOriginalVideoOnComplete;

this._vastPresented = true;
this.restoreVideoPlayer = true;

this.setInitialVolume();
this.addClassToVideo();
this.pauseExistingVideoSources();
this.setupQuartileSupport();
this.setupVideoEventListeners();
this.setupImpressions();
this.loadVastVideo();
this.playVideo();

if (this.autoplay) {
this.playVideo();
}
}

applyAsPrimary() {
this.applyAsPreroll();
this.restoreVideoPlayer = false;
applyAsPrimary(options = {}) {
this.applyAsPreroll(Object.assign({ restoreOriginalVideoOnComplete: false }, options));
}

// private
Expand Down Expand Up @@ -137,6 +153,13 @@ export default class VideoElement {
this.previousVolume = this.videoElement.muted ? -1 : this.videoElement.volume;
}

setInitialVolume() {
if (this.muted) {
this.videoElement.muted = true;
this.previousVolume = -1;
}
}

playVideo() {
this.videoElement.addEventListener('canplay', () => {
this.videoElement.play();
Expand Down
43 changes: 35 additions & 8 deletions src/lib/applications/video_js.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const EVENT_MAPPING = {
const VIDEO_CONTROLS_HEIGHT = 50;
const VAST_LOADED_CLASS = 'vast-running';
const VAST_PLAYING_CLASS = 'vast-playing';
const DEFAULT_OPTIONS = {
autoplay: false,
muted: true,
restoreOriginalVideoOnComplete: true,
};

export default class VideoJs {
constructor({ vast, videoJsPlayer }) {
Expand All @@ -17,25 +22,34 @@ export default class VideoJs {
this.previousSources = [];
this._vastPresented = null;
this.quartileSupport = new QuartileSupport();

this.restoreVideoPlayer = false;
this.autoplay = false;
this.muted = true;
}

applyAsPreroll() {
applyAsPreroll(options = {}) {
options = Object.assign({}, DEFAULT_OPTIONS, options);
this.autoplay = options.autoplay;
this.muted = options.muted;
this.restoreVideoPlayer = options.restoreOriginalVideoOnComplete;

this._vastPresented = true;
this.restoreVideoPlayer = true;

this.setInitialVolume();
this.addClassToVideo();
this.pauseExistingVideoSources();
this.setupQuartileSupport();
this.setupVideoEventListeners();
this.setupImpressions();
this.loadVastVideo();
this.autoPlayVideo();
if (this.autoplay) {
this.autoPlayVideo();
}
}

applyAsPrimary() {
this.applyAsPreroll();
this.restoreVideoPlayer = false;
applyAsPrimary(options = {}) {
this.applyAsPreroll(Object.assign({ restoreOriginalVideoOnComplete: false }, options));
}

// private
Expand Down Expand Up @@ -107,7 +121,6 @@ export default class VideoJs {
this.videoJsPlayer.removeClass(VAST_LOADED_CLASS);

this._vastPresented = false;
console.log('reloading previous sources', this.previousSources);
this.videoJsPlayer.src(this.previousSources);
}

Expand All @@ -116,12 +129,19 @@ export default class VideoJs {

if (this.previousVolume <= 0 && this.videoJsPlayer.volume() != 0) {
this.vast.addImpressionTrackingImagesFor('unmute');
} else if ((this.previousVolume > 0 && this.videoJsPlayer.volume() == 0) || this.videoJsPlayer.muted) {
} else if (this.previousVolume > 0 && (this.videoJsPlayer.volume() == 0 || this.videoJsPlayer.muted())) {
this.vast.addImpressionTrackingImagesFor('mute');
}
this.previousVolume = this.videoJsPlayer.muted() ? -1 : this.videoJsPlayer.volume();
}

setInitialVolume() {
if (this.muted) {
this.videoJsPlayer.muted(true);
this.previousVolume = -1;
}
}

autoPlayVideo() {
this.videoJsPlayer.ready(() => {
this.videoJsPlayer
Expand Down Expand Up @@ -155,6 +175,13 @@ export default class VideoJs {
}

setupQuartileSupport() {
const events = this.vast.trackingEventNamesWithOffsetPercent();
for (const name in events) {
this.quartileSupport.addEvent({
name: name,
offset: events[name],
});
}
this.quartileSupport.onQuartileChange(quartile => {
if (!this.vastPresented()) return;
this.vast.addImpressionTrackingImagesFor(quartile);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/node_value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default class NodeValue {
/**
* Returns the first TEXT or CDATA value from an XML element.
*
* @param {DOM Element} el An elemenet with a single CDATA or TEXT entity
*/
static fromElement(el) {
if (!el) return null;
const matchedItem = Array.from(el.childNodes).find(n => {
return (n.nodeType == Node.TEXT_NODE || n.nodeType == Node.CDATA_SECTION_NODE) && !!n.nodeValue.trim();
});
return matchedItem ? matchedItem.nodeValue.trim() : null;
}
}
Loading

0 comments on commit ac5455d

Please sign in to comment.