Skip to content

Commit

Permalink
Merge pull request #55 from voxmedia/jd-vast-hls-support
Browse files Browse the repository at this point in the history
Add HLS Support for VAST Videos
  • Loading branch information
jeninedrew authored Aug 29, 2023
2 parents 908a146 + 50fbf95 commit 14c6144
Show file tree
Hide file tree
Showing 17 changed files with 5,192 additions and 6,013 deletions.
23 changes: 0 additions & 23 deletions .circleci/config.yml

This file was deleted.

37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on: [push]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v3

- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '18.x'

- name: Get yarn cache
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"

- uses: actions/cache@v2
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
${{ github.workspace }}/.next/cache
.cache/Cypress
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install
run: yarn --frozen-lockfile --prefer-offline

- name: Run tests
run: yarn test
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v10.13.0
v18.12.0
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## v1.3.1 (April 16, 2019)
## v1.3.2 (August 29, 2023)

- Add HLS support for VAST videos

- ## v1.3.1 (April 16, 2019)

- Update Babel configs for IE11 and Jest

Expand Down
3,079 changes: 731 additions & 2,348 deletions dist/concert-vast.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "concert-vast",
"version": "1.3.1",
"name": "@voxmedia/concert-vast",
"version": "1.3.2",
"description": "Simple Vast Parsing for Concert Video Ads",
"main": "dist/concert-vast.js",
"author": "Vox Media",
Expand All @@ -10,7 +10,7 @@
"test": "jest",
"cypress": "cypress run",
"cypress-open": "cypress open",
"build": "webpack && pretty-quick dist/",
"build": "export NODE_OPTIONS=--openssl-legacy-provider && webpack && pretty-quick dist/",
"dev": "webpack-dev-server"
},
"devDependencies": {
Expand Down
17 changes: 12 additions & 5 deletions src/lib/applications/video_js.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import QuartileSupport from '../quartile_support';
import NodeValue from '../node_value';

const EVENT_MAPPING = {
muted: 'mute',
Expand Down Expand Up @@ -33,6 +34,7 @@ export default class VideoJs {
this.autoplay = options.autoplay;
this.muted = options.muted;
this.restoreVideoPlayer = options.restoreOriginalVideoOnComplete;
this.includeHlsSource = options.includeHlsSource;

this._vastPresented = true;

Expand Down Expand Up @@ -90,15 +92,20 @@ export default class VideoJs {
}

loadVastVideo() {
const bestVideo = this.vast.bestVideo({
const bestVideos = this.vast.bestVideo({
height: this.videoJsPlayer.height(),
width: this.videoJsPlayer.width(),
includeHlsSource: this.includeHlsSource,
});

this.videoJsPlayer.src({
type: bestVideo.mimeType(),
src: bestVideo.url(),
});
this.videoJsPlayer.src(
bestVideos.map(bestVideo => {
return {
type: bestVideo.element.getAttribute('type'),
src: NodeValue.fromElement(bestVideo.element),
};
})
);
}

updateQuartileDuration() {
Expand Down
16 changes: 12 additions & 4 deletions src/lib/stream_chooser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { supportedMimeTypes } from './supported_formats';
import { supportedMimeTypes, getHlsFormats } from './supported_formats';

export default class StreamChooser {
constructor() {
Expand Down Expand Up @@ -26,8 +26,12 @@ export default class StreamChooser {
this.bandwidthInKbs = bandwidthInKbs;
}

bestVideo() {
const matchingFormats = this.videos.filter(v => this.compatibleFormats(v));
hlsVideo() {
return getHlsFormats(this.videos);
}

bestStandardVideo() {
const matchingFormats = this.videos.filter(v => this.compatibleStandardFormats(v));
const closestSize = matchingFormats.sort((a, b) => this.closestSized(a, b));
const notExceedingBandwidth = closestSize.filter(v => this.underBandwidth(v));

Expand All @@ -38,12 +42,16 @@ export default class StreamChooser {
return notExceedingBandwidth[0];
}

bestVideos() {
return [this.hlsVideo(), this.bestStandardVideo()].filter(Boolean);
}

/**
* Returns true of this video is playable on the device/browser
* Follows the filter callback interface
* @param {MediaElement} video
*/
compatibleFormats(video) {
compatibleStandardFormats(video) {
return this.supportedMimeTypes.indexOf(video.mimeType()) != -1;
}

Expand Down
24 changes: 18 additions & 6 deletions src/lib/supported_formats.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
const FORMATS = {
const STANDARD_FORMATS = {
ogg: 'video/ogg; codecs="theora"',
h264: 'video/mp4; codecs="avc1.42E01E"',
webm: 'video/webm; codecs="vp8, vorbis"',
vp9: 'video/webm; codecs="vp9"',
hls: 'application/x-mpegURL; codecs="avc1.42E01E"',
};

const HLS_FORMATS = {
hls: 'application/vnd.apple.mpegurl',
hlsLegacy: 'application/x-mpegURL',
};

export function getHlsFormats(videos) {
return videos.find(v => hlsFormats(v));
}

function hlsFormats(video) {
return Object.values(HLS_FORMATS).indexOf(video.mimeType()) != -1;
}

export function supportedFormats({ doc } = { doc: document }) {
const v = doc.createElement('video');
let supported = {};
for (const name in FORMATS) {
if (v.canPlayType(FORMATS[name]) === 'probably') {
supported[name] = FORMATS[name];
for (const name in STANDARD_FORMATS) {
if (v.canPlayType(STANDARD_FORMATS[name]) === 'probably') {
supported[name] = STANDARD_FORMATS[name];
}
}
return supported;
Expand All @@ -21,4 +33,4 @@ export function supportedMimeTypes({ doc } = { doc: document }) {
return Object.values(supportedFormats({ doc: doc })).map(mimeTypeAndCode => mimeTypeAndCode.split(';')[0]);
}

export default { supportedFormats, supportedMimeTypes };
export default { supportedFormats, supportedMimeTypes, getHlsFormats };
5 changes: 3 additions & 2 deletions src/lib/vast.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,12 @@ export default class Vast {
}

bestVideo(
{ width, height, bandwidth, mimeTypes } = {
{ width, height, bandwidth, mimeTypes, includeHlsSource } = {
width: 800,
height: 600,
bandwidth: null,
mimeTypes: null,
includeHlsSource: false,
}
) {
const chooser = new StreamChooser();
Expand All @@ -133,7 +134,7 @@ export default class Vast {
if (mimeTypes) chooser.setSupportedMimeTypes(mimeTypes);

chooser.setPlayerDimensions({ width: width, height: height });
return chooser.bestVideo();
return includeHlsSource ? chooser.bestVideos() : chooser.bestStandardVideo();
}

async parse() {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/vast_elements/media_files.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class MediaFile {
}

isVideoType() {
return this.mimeType().match(/^video\//);
return this.mimeType().match(/^video\/|application\//);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/vast_elements/tracking_events.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default class TrackingEvents extends VastElementBase {
}

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

onVastReady() {
Expand Down
5 changes: 3 additions & 2 deletions test/fixtures/vast.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
<ClickThrough><![CDATA[https://adclick.g.doubleclick.net/pcs/click?xai=AKAOjsu6OYO6ScslHz6Ie0kqub5FCVUAxcJO1oOJ8eLjzG_onZ5wMdGPn-HE7YryuBvxvcekq_rLQrDY-aOhipXfK-hErA&sig=Cg0ArKJSzLIFAUkswSyTEAE&urlfix=1&adurl=https://nfl.demdex.net/event%3Fd_event%3Dclick%26d_adsrc%3D27510%26d_bu%3D901%26d_src%3D27511%26d_site%3D1106053%26d_campaign%3D21643693%26d_rd%3Dhttp://www.nfl.com]]></ClickThrough>
</VideoClicks>
<MediaFiles>
<MediaFile delivery="progressive" width="640" height="360" type="application/javascript" apiFramework="VPAID"><![CDATA[https://imasdk.googleapis.com/js/sdkloader/vpaid_adapter.js]]></MediaFile>
<MediaFile delivery="progressive" width="640" height="360" type="video/mp4" bitrate="262" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/18/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618308/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/974EA821C8BCBD176CED7FBDDDF84421BB96D3F7.B210716A5BB26DFD0F70F1D482AA150CB6B9D20/key/ck2/file/file.mp4]]></MediaFile>
<MediaFile delivery="progressive" width="1280" height="720" type="video/mp4" bitrate="1073" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/22/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618336/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/85C4C734DF46C98D2C02762819021751970FDEDE.2C279683EED540BE33E5C6BB55EF3B76E3BE5FB8/key/ck2/file/file.mp4]]></MediaFile>
<MediaFile delivery="progressive" width="1920" height="1080" type="video/mp4" bitrate="1369" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/37/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618354/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/7083BF2E2179B15C39B14F585A54B396966D5BD5.598147F619AF61C16FB6FC656CD7C98CE81BF64A/key/ck2/file/file.mp4]]></MediaFile>
Expand All @@ -39,7 +38,9 @@
<MediaFile delivery="progressive" width="640" height="360" type="video/mp4" bitrate="389" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/345/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618317/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/608E11DFDA27AFDB0D93859F9F7F4C2A0340921A.1FF0561324BC264C5B3C04FCD4B9CA5AF2B46B0F/key/ck2/file/file.mp4]]></MediaFile>
<MediaFile delivery="progressive" width="720" height="406" type="video/mp4" bitrate="700" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/346/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618314/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/434971CA7B0AD03623746F83C616513D53BC0C7A.7008BEFCBA52EF3F4366A1CBBE3D1AC91B2A67D7/key/ck2/file/file.mp4]]></MediaFile>
<MediaFile delivery="progressive" width="1024" height="576" type="video/mp4" bitrate="1408" scalable="false" maintainAspectRatio="false"><![CDATA[https://gcdn.2mdn.net/videoplayback/id/fadde8e0a3b24322/itag/347/source/doubleclick_dmm/ratebypass/yes/acao/yes/ip/0.0.0.0/ipbits/0/expire/3680618365/sparams/id,itag,source,ratebypass,acao,ip,ipbits,expire/signature/16DF08AE90C5CD4B6982EF86A488B281E198F121.242E75F4093594A221409AC9E32C92ED360E7444/key/ck2/file/file.mp4]]></MediaFile>
</MediaFiles>
<MediaFile delivery="progressive" width="640" height="360" type="application/javascript" apiFramework="VPAID"><![CDATA[https://imasdk.googleapis.com/js/sdkloader/vpaid_adapter.js]]></MediaFile>
<MediaFile delivery="progressive" width="256" height="144" type="application/x-mpegURL" bitrate="108" scalable="true" maintainAspectRatio="true"><![CDATA[ https://gcdn.2mdn.net/api/manifest/hls_variant/requiressl/yes/source/web_video_ads/id/800433178a2e04f3/itag/0/playlist_type/LIVE/ei/eOLLZPeuO_Lk-LYPrL--6AI/susc/daps/ctier/L/vprv/1/pacing/0/ip/0.0.0.0/ipbits/0/expire/3835531385/sparams/ip,ipbits,expire,requiressl,source,id,itag,playlist_type,ei,susc,ctier,vprv/signature/10B2A026CEAD5B26124D9D5E1788BE5C1779F776A3EBA256DEC20C081E117813.941098316F3985846893EE759B737E823E38D498AEB229C3928D44E0608B7A/key/us0/file/index.m3u8 ]]></MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
Expand Down
16 changes: 13 additions & 3 deletions test/stream_chooser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ describe('descision logic for StreamChooser', () => {
});

it('will return the choosen format', () => {
expect(typeof sc.bestVideo).toBe('function');
expect(typeof sc.bestStandardVideo).toBe('function');
});

it('will choose the right format for supported formats', () => {
sc.setSupportedMimeTypes(['video/mp4']);
sc.useVideosFromMediaFile(vast.videos());
sc.setBandwidth(1500);
sc.setPlayerDimensions({ width: 1200, height: 720 });
const vid = sc.bestVideo();
const vid = sc.bestStandardVideo();
expect(vid).not.toBe(null);
expect(vid.constructor.name).toBe('MediaFile');
expect(vid.mimeType()).toBe('video/mp4');
Expand All @@ -50,12 +50,22 @@ describe('descision logic for StreamChooser', () => {
sc.useVideosFromMediaFile(vast.videos());
sc.setBandwidth(600);
sc.setPlayerDimensions({ width: 500, height: 300 });
const vid = sc.bestVideo();
const vid = sc.bestStandardVideo();
expect(vid).not.toBe(null);
expect(vid.constructor.name).toBe('MediaFile');
expect(vid.mimeType()).toBe('video/mp4');
expect(vid.width()).toBe(480);
expect(vid.height()).toBe(270);
expect(vid.bitrate()).toBe(385);
});

it('will choose the right formats when HLS is supported', () => {
sc.setSupportedMimeTypes(['video/mp4']);
sc.useVideosFromMediaFile(vast.videos());
const vid = sc.bestVideos();
expect(vid).not.toBe(null);
expect(Array.isArray(vid)).toBe(true);
expect(vid[0].mimeType()).toBe('application/x-mpegURL');
expect(vid[1].mimeType()).toBe('video/mp4');
});
});
2 changes: 1 addition & 1 deletion test/vast.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('Vast Videos', () => {
it('can return a list of video files', () => {
const vast = new Vast({ xml: xmlString });
const videos = vast.videos();
expect(videos.length).toBe(10);
expect(videos.length).toBe(12);
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/vast_elements/media_files.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Media Files extension', () => {

it('can return a list of video files', () => {
const videos = vast.videos();
expect(videos.length).toBe(10);
expect(videos.length).toBe(12);
});

it('can get the url of the video', () => {
Expand Down
Loading

0 comments on commit 14c6144

Please sign in to comment.