From b924921c658caf814a66af7b6c962f36da39b8a1 Mon Sep 17 00:00:00 2001 From: oscnord <6131004+oscnord@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:02:03 +0100 Subject: [PATCH] feat: support multiple VAST versions --- README.md | 21 +++++--- api/Session.js | 2 + api/routes.js | 13 +++++ test/Session.spec.js | 5 +- utils/vast-maker.js | 115 +++++++++++++++++++++++++++---------------- utils/vmap-maker.js | 1 + 6 files changed, 104 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 705f02a..4a865b0 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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: + ``` @@ -96,13 +101,14 @@ The test-adserver is expecting an mRSS feed which should include text/xml with t ``` -Simply populate your xml file with `` tags for each Ad asset with the necessary data (id, universalId, link, duration, etc...). + +Simply populate your xml file with `` 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 `` 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 sales@eyevinn.se 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 if you are interested for pricing and more information. ### Hosting @@ -110,7 +116,7 @@ We host the service in our environment for a monthly recurring fee. Included is ### 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 @@ -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 work@eyevinn.se! +Want to know more about Eyevinn and how it is to work here. Contact us at ! diff --git a/api/Session.js b/api/Session.js index cb7d274..c70ec26 100644 --- a/api/Session.js +++ b/api/Session.js @@ -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; @@ -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; diff --git a/api/routes.js b/api/routes.js index fe60a2e..fad2d36 100644 --- a/api/routes.js +++ b/api/routes.js @@ -282,6 +282,7 @@ const SessionSchema = () => ({ min: "10", max: "45", ps: "4", + v: "4", }, response: "", }, @@ -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", @@ -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", diff --git a/test/Session.spec.js b/test/Session.spec.js index 99b9204..91a2e95 100644 --- a/test/Session.spec.js +++ b/test/Session.spec.js @@ -12,6 +12,7 @@ mockClientParams1 = { dt: "mobile", ss: "1920x1080", uip: "123.23.32.13", + v: "3", }; mockClientParams2 = { c: true, @@ -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"); @@ -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"); diff --git a/utils/vast-maker.js b/utils/vast-maker.js index 091e8d8..8be6f2d 100644 --- a/utils/vast-maker.js +++ b/utils/vast-maker.js @@ -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) { @@ -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 -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}`), @@ -147,14 +161,16 @@ 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" }) @@ -162,25 +178,34 @@ function AttachStandAloneAds(vast4, ads, params, podSize) { .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}`), @@ -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', @@ -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', @@ -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', diff --git a/utils/vmap-maker.js b/utils/vmap-maker.js index eb0ecfa..a183433 100644 --- a/utils/vmap-maker.js +++ b/utils/vmap-maker.js @@ -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))) : [];