Skip to content

Commit

Permalink
Merge pull request #19 from voxmedia/ba-add-progress-tracking-events
Browse files Browse the repository at this point in the history
Add Progress Tracking Events
  • Loading branch information
Brian Anderson authored Mar 17, 2019
2 parents e0885d8 + d0ccaf2 commit e9b2aeb
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 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
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'],
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "concert-vast",
"version": "1.1.0",
"version": "1.2.0",
"description": "Simple Vast Parsing for Concert Video Ads",
"main": "src/index.js",
"author": "Vox Media",
Expand Down
7 changes: 7 additions & 0 deletions src/lib/applications/video_js.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,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
1 change: 1 addition & 0 deletions src/lib/node_value.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default class NodeValue {
* @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();
});
Expand Down
15 changes: 14 additions & 1 deletion src/lib/quartile_support.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@ const QUARTILES = [
];

export default class QuartileSupport {
static quartiles() {
let quartiles = {};
QUARTILES.forEach(quart => {
quartiles[quart[1]] = quart[0];
});
return quartiles;
}

constructor() {
this.quartiles = QUARTILES.slice(0);
this.seenQuartiles = [];
this.callbacks = [];
this.currentTime = 0;
this.duration = Infinity;
}

addEvent({ name, offset }) {
this.quartiles.push([offset, name]);
}

setDuration(time) {
if (time != 0) {
this.duration = time;
Expand All @@ -34,7 +47,7 @@ export default class QuartileSupport {

checkForQuartileEvent() {
const percentComplete = this.currentTime / this.duration;
const matchingQuartiles = QUARTILES.filter(quartile => {
const matchingQuartiles = this.quartiles.filter(quartile => {
return quartile[0] < percentComplete;
});

Expand Down
53 changes: 53 additions & 0 deletions src/lib/timecodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export default class Timecode {
/**
* Converts seconds to a three segment timecode hh:mm:ss
* Examples: 30 => "00:00:30"
* 90 => "00:01:30"
*
* @param {String|Number} maybeSeconds
* @returns {String} "hh:mm:ss" timecode
*/
static secondsToTimecode(maybeSeconds) {
const seconds = Number(maybeSeconds);

if (isNaN(seconds) || maybeSeconds.toString().indexOf('%') != -1) {
return null;
}

const date = new Date(null);
date.setSeconds(seconds);
return date.toISOString().substr(11, 8);
}

/**
* Converts timecode hh:mm:ss to total seconds
* Examples: "00:00:30" => 30
* "00:10:10" => 610
*
* @param {String} timecode Timecode input
* @returns {Number} the number of seconds represented in the timecode
*/
static timecodeToSeconds(timecode) {
let parts = timecode.split(':');
let seconds = 0;
let multiplier = 1;

while (parts.length > 0) {
seconds += multiplier * parseInt(parts.pop(), 10);
multiplier *= 60;
}
return seconds;
}

/**
* Will properly format the timecode into three segment parts
* Example: "01:10" => "00:01:10"
*
* @param {String} code input timecode
* @returns {String} three segment timecode hh:mm:ss
*/
static timecodeToTimecode(code) {
const result = this.secondsToTimecode(this.timecodeToSeconds(code));
return result;
}
}
8 changes: 8 additions & 0 deletions src/lib/vast.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ export default class Vast {
return this.loadedElements['TrackingEvents'].trackingUrlsFor(eventName);
}

trackingEventNamesWithOffsets() {
return this.loadedElements['TrackingEvents'].trackingEventNamesWithOffsets();
}

trackingEventNamesWithOffsetPercent() {
return this.loadedElements['TrackingEvents'].trackingEventNamesWithOffsetPercent();
}

addImpressionTrackingImagesFor(eventName, doc = document) {
return this.loadedElements['TrackingEvents'].addImpressionTrackingImagesFor(eventName, doc);
}
Expand Down
110 changes: 104 additions & 6 deletions src/lib/vast_elements/tracking_events.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,126 @@
import VastElementBase from './vast_element_base';
import Timecodes from '../timecodes';
import NodeValue from '../node_value';
import QuartileSupport from '../quartile_support';

class TrackingEvent {
constructor({ eventName, url }) {
this.eventName = eventName;
this._url = url;
}

matches(string) {
return this.eventName == string;
}

name() {
return this.eventName;
}

offsetInSeconds(duration) {
const quarts = QuartileSupport.quartiles();
return quarts[this.eventName] * duration;
}

offsetInPercent(duration) {
const quarts = QuartileSupport.quartiles();
return quarts[this.eventName];
}

url() {
return this._url;
}
}

class ProgressTrackingEvent {
constructor({ offset, url }) {
this.offset = offset;
this._url = url;
}

matches(secondsOrTimeCodeOrPercent) {
return (
this.offset == secondsOrTimeCodeOrPercent ||
this.offset == Timecodes.secondsToTimecode(secondsOrTimeCodeOrPercent) ||
this.offset == Timecodes.timecodeToTimecode(secondsOrTimeCodeOrPercent)
);
}

name() {
return this.offset;
}

offsetInSeconds(duration) {
if (this.offset.indexOf('%') != -1) {
return duration * (Number(this.offset.replace('%', '')) / 100.0);
} else {
return Timecodes.timecodeToSeconds(this.offset);
}
}

offsetInPercent(duration) {
return Math.min(1.0, this.offsetInSeconds(duration) / duration);
}

url() {
return this._url;
}
}

export default class TrackingEvents extends VastElementBase {
setup() {
this.trackingUrls = [];
this.trackingEvents = [];
this.duration = undefined;
}

static selector() {
return 'Ad TrackingEvents Tracking';
return 'Ad TrackingEvents Tracking, Ad Inline Creative Duration';
}

onVastReady() {
this.trackingUrls = this.elements.map(el => {
return [el.getAttribute('event'), NodeValue.fromElement(el)];
});
const durationValue = NodeValue.fromElement(this.elements.find(el => el.nodeName == 'Duration'));
this.duration = Timecodes.timecodeToSeconds(durationValue || '0');

this.trackingEvents = this.elements
.filter(el => el.nodeName != 'Duration')
.map(el => {
const trackingEvent = el.hasAttribute('offset') ? ProgressTrackingEvent : TrackingEvent;

return new trackingEvent({
eventName: el.getAttribute('event'),
offset: el.getAttribute('offset'),
url: NodeValue.fromElement(el),
});
});
}

trackingUrlsFor(eventName) {
return this.trackingUrls.filter(t => t[0] == eventName).map(t => t[1]);
return this.trackingEvents.filter(t => t.matches(eventName)).map(t => t.url());
}

addImpressionTrackingImagesFor(eventName, doc = document) {
this.trackingUrlsFor(eventName).forEach(url => {
this.addImpressionUrl(url, { doc: doc, name: eventName });
});
}

trackingEventNamesWithOffsets() {
return this.trackingEvents.reduce((all, event) => {
const offsetSeconds = event.offsetInSeconds(this.duration);
if (offsetSeconds) {
all[event.name()] = offsetSeconds;
}
return all;
}, {});
}

trackingEventNamesWithOffsetPercent() {
return this.trackingEvents.reduce((all, event) => {
const offsetPercent = event.offsetInPercent(this.duration);
if (offsetPercent) {
all[event.name()] = offsetPercent;
}
return all;
}, {});
}
}
97 changes: 51 additions & 46 deletions test/fixtures/vast-progress.xml
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
<VAST version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Ad id="20001">
<InLine>
<AdSystem version="4.0">iabtechlab</AdSystem>
<AdTitle>iabtechlab video ad</AdTitle>
<Pricing model="cpm" currency="USD">
<![CDATA[ 25.00 ]]>
</Pricing>
<Error>http://example.com/error</Error>
<Impression id="Impression-ID">http://example.com/track/impression</Impression>
<Creatives>
<Creative id="5480" sequence="1">
<Linear>
<Duration>00:00:16</Duration>
<TrackingEvents>
<Tracking event="start">http://example.com/tracking/start</Tracking>
<Tracking event="firstQuartile">http://example.com/tracking/firstQuartile</Tracking>
<Tracking event="midpoint">http://example.com/tracking/midpoint</Tracking>
<Tracking event="thirdQuartile">http://example.com/tracking/thirdQuartile</Tracking>
<Tracking event="complete">http://example.com/tracking/complete</Tracking>
<Tracking event="progress" offset="00:00:10">http://example.com/tracking/progress-10</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickThrough id="blog">
<![CDATA[https://iabtechlab.com]]>
</ClickThrough>
</VideoClicks>
<MediaFiles>
<MediaFile id="5241" delivery="progressive" type="video/mp4" bitrate="500" width="400" height="300" minBitrate="360" maxBitrate="1080" scalable="1" maintainAspectRatio="1" codec="0" apiFramework="VAST">
<![CDATA[https://iab-publicfiles.s3.amazonaws.com/vast/VAST-4.0-Short-Intro.mp4]]>
</MediaFile>
</MediaFiles>


</Linear>
</Creative>
</Creatives>
<Extensions>
<Extension type="iab-Count">
<total_available>
<![CDATA[ 2 ]]>
</total_available>
</Extension>
</Extensions>
</InLine>
</Ad>
<VAST version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Ad id="20009">
<InLine>
<AdSystem version="4.0">iabtechlab</AdSystem>
<AdTitle>iabtechlab video ad</AdTitle>
<Pricing model="cpm" currency="USD">
<![CDATA[ 25.00 ]]>
</Pricing>
<Error>http://example.com/error</Error>
<Impression id="Impression-ID">http://example.com/track/impression</Impression>
<Creatives>
<Creative id="5480" sequence="1">
<Linear>
<Duration>00:00:16</Duration>
<TrackingEvents>
<Tracking event="start">http://example.com/tracking/start</Tracking>
<Tracking event="firstQuartile">http://example.com/tracking/firstQuartile</Tracking>
<Tracking event="midpoint">http://example.com/tracking/midpoint</Tracking>
<Tracking event="thirdQuartile">http://example.com/tracking/thirdQuartile</Tracking>
<Tracking event="complete">http://example.com/tracking/complete</Tracking>
<Tracking event="progress" offset="00:00:10">http://example.com/tracking/progress-10</Tracking>
<Tracking event="progress" offset="00:01:40">http://example.com/tracking/progress-140</Tracking>
<Tracking event="progress" offset="15%">http://example.com/tracking/progress-15-percent</Tracking>
<Tracking event="progress" offset="50%">http://example.com/tracking/progress-50-percent</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickThrough id="blog">
<![CDATA[https://iabtechlab.com]]>
</ClickThrough>
<ClickTracking>
<![CDATA[http://example.com/trackingurl/clickTracking]]>
</ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile id="5241" delivery="progressive" type="video/mp4" bitrate="500" width="400" height="300" minBitrate="360" maxBitrate="1080" scalable="1" maintainAspectRatio="1" codec="0">
<![CDATA[https://iab-publicfiles.s3.amazonaws.com/vast/VAST-4.0-Short-Intro.mp4]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
<Extensions>
<Extension type="iab-Count">
<total_available>
<![CDATA[ 2 ]]>
</total_available>
</Extension>
</Extensions>
</InLine>
</Ad>
</VAST>
Loading

0 comments on commit e9b2aeb

Please sign in to comment.