Skip to content

Commit

Permalink
feat: support multiple VAST versions
Browse files Browse the repository at this point in the history
  • Loading branch information
oscnord committed Jan 31, 2024
1 parent 7b51292 commit b924921
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 53 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ The Eyevinn Test Adserver is an adserver that can be used in different testing c
This component is released under open source and we are happy for contributions!

## Requirements

- Node v12+

## Database

Right now the test-adserver uses in-memory storage for all its data, no external database is required.

In a future update, we will add support for persistent storage using PostgreSQL.
Other databases can be used also, as long as they follow the same implementation steps that of the coming PostgreSQL example.
Other databases can be used also, as long as they follow the same implementation steps that of the coming PostgreSQL example.

## Usage

## Usage
- `git clone https://github.com/Eyevinn/test-adserver.git`
- `cd test-adserver`
- `npm install`, then
Expand Down Expand Up @@ -62,8 +65,8 @@ Stop the service:

docker-compose down


## Using Specific Ads

If the enviroment variable `MRSS_ORIGIN` has been set, then the test-adserver shall return VAST responses populated with Ads selected from
the collection of Ads found in the mRSS feed that can be reached through this origin endpoint. The url for the feed should follow this structure
`${MRSS_ORIGIN}${ADSERVER_HOST}.mrss`. Where `ADSERVER_HOST` is the same as the host data that can be found in the request headers sent to the test-adserver.
Expand All @@ -77,7 +80,9 @@ Knowing the adserver host and `MRSS_ORIGIN`, the test-adserver will then fetch t
Alternatively, you can specify what file contains the collection of ads through the `coll` parameter on the `/api/v1/vast` or `/api/v1/vmap` request. In this case, the file will be expected to be at `${MRSS_ORIGIN}${coll}.mrss`. This is useful for example if you want to switch easily between different collection of ads without having to host multiple ad servers.

### MRSS Feed Structure

The test-adserver is expecting an mRSS feed which should include text/xml with the following structure:

```
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
Expand All @@ -96,21 +101,22 @@ The test-adserver is expecting an mRSS feed which should include text/xml with t
</entry>
</feed>
```
Simply populate your xml file with `<entry></entry>` tags for each Ad asset with the necessary data (id, universalId, link, duration, etc...).

Simply populate your xml file with `<entry></entry>` tags for each Ad asset with the necessary data (id, universalId, link, duration, etc...).

If you have ads in multiple formats (eg. DASH, HLS, MP4), you can add multiple `<link></link>` for each one.

## Commercial Options

The Eyevinn Test Adserver is released under open source but we do offer some commercial options in relation to it. Contact [email protected] if you are interested for pricing and more information.
The Eyevinn Test Adserver is released under open source but we do offer some commercial options in relation to it. Contact <[email protected]> if you are interested for pricing and more information.

### Hosting

We host the service in our environment for a monthly recurring fee. Included is business hours support on a best effort basis.

### Deployment

We help you deploy and integrate the service in your environment on a time-of-material basis.
We help you deploy and integrate the service in your environment on a time-of-material basis.

### Feature Development

Expand All @@ -120,11 +126,10 @@ When you need a new feature developed and does not have the capacity or competen

When you need help with building for example integration adaptors or other development in your code base related to this open source project we can offer a development team from us to help out on a time-of-material basis.


## About Eyevinn Technology

Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor.

At Eyevinn, every software developer consultant has a dedicated budget reserved for open source development and contribution to the open source community. This give us room for innovation, team building and personal competence development. And also gives us as a company a way to contribute back to the open source community.

Want to know more about Eyevinn and how it is to work here. Contact us at [email protected]!
Want to know more about Eyevinn and how it is to work here. Contact us at <[email protected]>!
2 changes: 2 additions & 0 deletions api/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Session {
minPodDuration: params.min || null,
podSize: params.ps || null,
adCollection: params.coll || null,
version: params.v || null,
},
});
this.#vmapXml = vmapObj.xml;
Expand All @@ -62,6 +63,7 @@ class Session {
minPodDuration: params.min || null,
podSize: params.ps || null,
adCollection: params.coll || null,
version: params.v || null,
});
this.#vastXml = vastObj.xml;
this.adBreakDuration = vastObj.duration;
Expand Down
13 changes: 13 additions & 0 deletions api/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ const SessionSchema = () => ({
min: "10",
max: "45",
ps: "4",
v: "4",
},
response: "<VAST XML>",
},
Expand Down Expand Up @@ -602,6 +603,12 @@ const schemas = {
description: "Desired Pod size in numbers of Ads.",
example: "3",
},
v: {
type: "string",
description:
"VAST version to use. Default is 4. Supported values are 2, 3 and 4",
example: "3",
},
userAgent: {
type: "string",
description: "Client's user agent",
Expand Down Expand Up @@ -697,6 +704,12 @@ const schemas = {
"Desired Pod size in midroll adbreak, in numbers of Ads.",
example: "3",
},
v: {
type: "string",
description:
"VAST version to use. Default is 4. Supported values are 2, 3 and 4.",
example: "3",
},
userAgent: {
type: "string",
description: "Client's user agent",
Expand Down
5 changes: 3 additions & 2 deletions test/Session.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mockClientParams1 = {
dt: "mobile",
ss: "1920x1080",
uip: "123.23.32.13",
v: "3",
};
mockClientParams2 = {
c: true,
Expand Down Expand Up @@ -55,7 +56,7 @@ describe("SESSION CLASS", () => {
session1.AddTrackedEvent(mockTrackedEvent1);
session1.AddTrackedEvent(mockTrackedEvent2);
session1.AddTrackedEvent(mockTrackedEvent3);

const eventsObj = session1.getTrackedEvents();

eventsObj.should.be.a("object");
Expand All @@ -69,7 +70,7 @@ describe("SESSION CLASS", () => {
session1.AddTrackedEvent(mockTrackedEvent1);
session1.AddTrackedEvent(mockTrackedEvent2);
session1.AddTrackedEvent(mockTrackedEvent3);

const eventsObj = session1.getTrackedEvents();

eventsObj.should.be.a("object");
Expand Down
115 changes: 72 additions & 43 deletions utils/vast-maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,13 @@ const DEFAULT_AD_LIST = [
* podSize: "3",
* podMin: "10",
* podMax: "40"
* }
* version: "2", "3", "4" (default)
*
*/
function VastBuilder(params) {
let vastObject = {};
let adList = [];
let vast = null;
// Use Default AdList OR get new List from TENANT_CACHE.
let tenantId;
if (params.adserverHostname) {
Expand All @@ -109,34 +111,46 @@ function VastBuilder(params) {
params.maxPodDuration
);

switch (params.version) {
case "2":
vast = createVast.v2();
break;
case "3":
vast = createVast.v3();
break;
default:
vast = createVast.v4();
break;
}
//selectedAds = selectedAds.standAloneAds;
const vast4 = createVast.v4();

AttachPodAds(vast4, selectedAds.podAds, params);
AttachStandAloneAds(vast4, selectedAds.standAloneAds, params, selectedAds.podAds.length);
AttachPodAds(vast, selectedAds.podAds, params);
AttachStandAloneAds(vast, selectedAds.standAloneAds, params, selectedAds.podAds.length);

vastObject = { xml: vast4.toXml(), duration: adsDuration };
vastObject = { xml: vast.toXml(), duration: adsDuration };

return vastObject;
}

// Add <Ad>-tags for every ad in the sampleAds list
function AttachStandAloneAds(vast4, ads, params, podSize) {
function AttachStandAloneAds(vast, ads, params, podSize) {
podSize = podSize ? podSize + 1 : 1;
const adId = vast.attrs.version === "4.0" ? "adId" : "adID";
for (let i = 0; i < ads.length; i++) {
vast4
let mediaNode = vast
.attachAd({ id: `AD-ID_00${i + podSize}` })
.attachInLine()
.addImpression(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=vast`, { id: `IMPRESSION-ID_00${i + podSize}` })
.addError(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=e`)
.addImpression(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=vast`, { id: `IMPRESSION-ID_00${i + podSize}` })
.addError(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=e`)
.addAdSystem(`Test Adserver`)
.addAdTitle(`Ad That Test-Adserver Wants Player To See #${i + podSize}`)
.attachCreatives()
.attachCreative({
id: `CREATIVE-ID_00${i + podSize}`,
adId: `${ads[i].id}`,
[adId]: `${ads[i].id}`,
sequence: `${i + podSize}`,
})
});
if (vast.attrs.version === "4.0") {
mediaNode = mediaNode
.addUniversalAdId(encodeURIComponent(`${ads[i].universalId}${i + podSize}`), {
idRegistry: "test-ad-id.eyevinn",
idValue: encodeURIComponent(`${ads[i].universalId}${i + podSize}`),
Expand All @@ -147,40 +161,51 @@ function AttachStandAloneAds(vast4, ads, params, podSize) {
idRegistry: 'test-ad-id.eyevinn',
idValue: encodeURIComponent(`${ads[i].universalId}${i + podSize}`),
}
)
);
}
mediaNode = mediaNode
.attachLinear()
.attachTrackingEvents()
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=0`, { event: "start" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=25`, { event: "firstQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=50`, { event: "midpoint" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=75`, { event: "thirdQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${ads[i].id}&progress=100`, { event: "complete" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=0`, { event: "start" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=25`, { event: "firstQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=50`, { event: "midpoint" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=75`, { event: "thirdQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${ads[i].id}&progress=100`, { event: "complete" })
.and()
.attachVideoClicks()
.addClickThrough("https://github.com/Eyevinn/test-adserver", { id: "Eyevinn Test AdServer" })
.and()
.addDuration(ads[i].duration)
.attachMediaFiles();

AddMediaFiles(vast4, ads[i].url, ads[i].bitrate, ads[i].width, ads[i].height, ads[i].codec)
AddMediaFiles(mediaNode, ads[i].url, ads[i].bitrate, ads[i].width, ads[i].height, ads[i].codec)
}
}

// Attaching Pod adds to the VAST object.
function AttachPodAds(vast4, podAds, params) {
function AttachPodAds(vast, podAds, params) {
// ad-id is adID in VAST 2.0 and 3.0, adId in VAST 4.0
const adId = vast.attrs.version === "4.0" ? "adId" : "adID";
for (let i = 0; i < podAds.length; i++) {
mediaNode = vast4
.attachAd({ id: `POD_AD-ID_00${i + 1}`, sequence: `${i + 1}` })
let attachAdParams = { id: `POD_AD-ID_00${i + 1}` };
// VAST 2.0 does not support sequence attribute.
if (vast.attrs.version !== "2.0") {
attachAdParams.sequence = `${i + 1}`;
}
let mediaNode = vast
.attachAd(attachAdParams)
.attachInLine()
.addImpression(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=vast`, { id: `IMPRESSION-ID_00${i + 1}` })
.addImpression(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=vast`, { id: `IMPRESSION-ID_00${i + 1}` })
.addAdSystem(`Test Adserver`)
.addAdTitle(`Ad That Test-Adserver Wants Player To See #${i + 1}`)
.attachCreatives()
.attachCreative({
id: `CRETIVE-ID_00${i + 1}`,
adId: `${podAds[i].id}_${i + 1}`,
[adId]: `${podAds[i].id}_${i + 1}`,
sequence: `${i + 1}`,
})
});
if (vast.attrs.version === "4.0") {
mediaNode = mediaNode
.addUniversalAdId(encodeURIComponent(`${podAds[i].universalId}${i + 1}`), {
idRegistry: "test-ad-id.eyevinn",
idValue: encodeURIComponent(`${podAds[i].universalId}${i + 1}`),
Expand All @@ -191,35 +216,39 @@ function AttachPodAds(vast4, podAds, params) {
idRegistry: 'test-ad-id.eyevinn',
idValue: encodeURIComponent(`${podAds[i].universalId}${i + 1}`),
}
)
);
}
mediaNode = mediaNode
.attachLinear()
.attachTrackingEvents()
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=0`, { event: "start" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=25`, { event: "firstQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=50`, { event: "midpoint" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=75`, { event: "thirdQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?adId=${podAds[i].id}_${i + 1}&progress=100`, { event: "complete" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=0`, { event: "start" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=25`, { event: "firstQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=50`, { event: "midpoint" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=75`, { event: "thirdQuartile" })
.addTracking(`http://${params.adserverHostname}/api/v1/sessions/${params.sessionId}/tracking?${adId}=${podAds[i].id}_${i + 1}&progress=100`, { event: "complete" })
.and()
.attachVideoClicks()
.addClickThrough("https://github.com/Eyevinn/test-adserver", { id: "Eyevinn Test AdServer" })
.and()
.addDuration(podAds[i].duration)
.attachMediaFiles();
AddMediaFiles(mediaNode, podAds[i].url, podAds[i].bitrate, podAds[i].width, podAds[i].height, podAds[i].codec)

AddMediaFiles(mediaNode, podAds[i].url, podAds[i].bitrate, podAds[i].width, podAds[i].height, podAds[i].codec, vast.attrs.version)
}
}

function AddMediaFiles(vast4MediaFilesNode, urls, bitrate, width, height, codec) {
function AddMediaFiles(vastMediaFilesNode, urls, bitrate, width, height, codec, version) {
const mediaFile = {
width: width,
height: height,
}
// VAST 2.0 does not support codec attribute.
if (version !== "2.0") {
mediaFile.codec = codec;
}
for (let i = 0; i < urls.length; i++) {
mediaFile = {
width: width,
height: height,
codec: codec,
}

if (urls[i].endsWith(".mp4")) {
vast4MediaFilesNode
vastMediaFilesNode
.attachMediaFile(urls[i],
Object.assign(mediaFile, {
delivery: 'progressive',
Expand All @@ -229,7 +258,7 @@ function AddMediaFiles(vast4MediaFilesNode, urls, bitrate, width, height, codec)
.back();
}
if (urls[i].endsWith(".m3u8")) {
vast4MediaFilesNode
vastMediaFilesNode
.attachMediaFile(urls[i],
Object.assign(mediaFile, {
delivery: 'streaming',
Expand All @@ -240,7 +269,7 @@ function AddMediaFiles(vast4MediaFilesNode, urls, bitrate, width, height, codec)
.back();
}
if (urls[i].endsWith(".mpd")) {
vast4MediaFilesNode
vastMediaFilesNode
.attachMediaFile(urls[i],
Object.assign(mediaFile, {
delivery: 'streaming',
Expand Down
1 change: 1 addition & 0 deletions utils/vmap-maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function VmapBuilder(params) {
minPodDuration: null,
podSize: null,
adCollection: GVC.adCollection,
version: GVC.version,
};

const breakpoints = params.breakpoints ? params.breakpoints.split(",").filter((item) => !isNaN(Number(item))) : [];
Expand Down

0 comments on commit b924921

Please sign in to comment.