Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adagioBidAdapter: add Video support #6038

Merged
merged 5 commits into from
Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 146 additions & 17 deletions modules/adagioBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ import find from 'core-js-pure/features/array/find.js';
import * as utils from '../src/utils.js';
import { config } from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import { loadExternalScript } from '../src/adloader.js'
import { loadExternalScript } from '../src/adloader.js';
import JSEncrypt from 'jsencrypt/bin/jsencrypt.js';
import sha256 from 'crypto-js/sha256.js';
import { getStorageManager } from '../src/storageManager.js';
import { getRefererInfo } from '../src/refererDetection.js';
import { createEidsArray } from './userId/eids.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { Renderer } from '../src/Renderer.js';
import { OUTSTREAM } from '../src/video.js';

export const BIDDER_CODE = 'adagio';
export const LOG_PREFIX = 'Adagio:';
export const VERSION = '2.5.0';
export const VERSION = '2.6.0';
export const FEATURES_VERSION = '1';
export const ENDPOINT = 'https://mp.4dex.io/prebid';
export const SUPPORTED_MEDIA_TYPES = ['banner'];
export const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO];
export const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js';
export const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript';
export const GVLID = 617;
export const storage = getStorageManager(GVLID, 'adagio');
export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js';

export const ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0
Expand All @@ -27,6 +31,35 @@ t0b0lsHN+W4n9kitS/DZ/xnxWK/9vxhv0ZtL1LL/rwR5Mup7rmJbNtDoNBw4TIGj
pV6EP3MTLosuUEpLaQIDAQAB
-----END PUBLIC KEY-----`;

// This provide a whitelist and a basic validation
// of OpenRTB 2.5 options used by the Adagio SSP.
// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf
export const ORTB_VIDEO_PARAMS = {
'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'),
'minduration': (value) => utils.isInteger(value),
'maxduration': (value) => utils.isInteger(value),
'protocols': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].indexOf(v) !== -1),
'w': (value) => utils.isInteger(value),
'h': (value) => utils.isInteger(value),
'startdelay': (value) => utils.isInteger(value),
'placement': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5].indexOf(v) !== -1),
'linearity': (value) => [1, 2].indexOf(value) !== -1,
'skip': (value) => [0, 1].indexOf(value) !== -1,
'skipmin': (value) => utils.isInteger(value),
'skipafter': (value) => utils.isInteger(value),
'sequence': (value) => utils.isInteger(value),
'battr': (value) => Array.isArray(value) && value.every(v => Array.from({length: 17}, (_, i) => i + 1).indexOf(v) !== -1),
'maxextended': (value) => utils.isInteger(value),
'minbitrate': (value) => utils.isInteger(value),
'maxbitrate': (value) => utils.isInteger(value),
'boxingallowed': (value) => [0, 1].indexOf(value) !== -1,
'playbackmethod': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1),
'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1,
'delivery': (value) => [1, 2, 3].indexOf(value) !== -1,
'pos': (value) => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1,
'api': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1)
};

let currentWindow;

export function adagioScriptFromLocalStorageCb(ls) {
Expand Down Expand Up @@ -64,7 +97,7 @@ export function adagioScriptFromLocalStorageCb(ls) {

export function getAdagioScript() {
storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => {
internal.adagioScriptFromLocalStorageCb(ls)
internal.adagioScriptFromLocalStorageCb(ls);
});

storage.localStorageIsEnabled(isValid => {
Expand Down Expand Up @@ -335,7 +368,7 @@ function getOrAddAdagioAdUnit(adUnitCode) {
w.ADAGIO = w.ADAGIO || {};

if (w.ADAGIO.adUnits[adUnitCode]) {
return w.ADAGIO.adUnits[adUnitCode]
return w.ADAGIO.adUnits[adUnitCode];
}

return w.ADAGIO.adUnits[adUnitCode] = {};
Expand Down Expand Up @@ -435,7 +468,7 @@ function getElementFromTopWindow(element, currentWindow) {
};

function autoDetectAdUnitElementId(adUnitCode) {
const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode)
const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode);
let adUnitElementId = null;

if (autoDetectedAdUnit && autoDetectedAdUnit.divId) {
Expand All @@ -450,16 +483,16 @@ function autoDetectEnvironment() {
let environment;
switch (device) {
case 2:
environment = 'desktop'
environment = 'desktop';
break;
case 4:
environment = 'mobile'
environment = 'mobile';
break;
case 5:
environment = 'tablet'
environment = 'tablet';
break;
};
return environment
return environment;
};

function getFeatures(bidRequest, bidderRequest) {
Expand Down Expand Up @@ -507,6 +540,21 @@ function getFeatures(bidRequest, bidderRequest) {
return features;
};

function isRendererPreferredFromPublisher(bidRequest) {
// renderer defined at adUnit level
const adUnitRenderer = utils.deepAccess(bidRequest, 'renderer');
const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render);

// renderer defined at adUnit.mediaTypes level
const mediaTypeRenderer = utils.deepAccess(bidRequest, 'mediaTypes.video.renderer');
const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render);

return !!(
(hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) ||
(hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true))
);
}

export const internal = {
enqueue,
getOrAddAdagioAdUnit,
Expand All @@ -521,7 +569,8 @@ export const internal = {
getRefererInfo,
adagioScriptFromLocalStorageCb,
getCurrentWindow,
canAccessTopWindow
canAccessTopWindow,
isRendererPreferredFromPublisher
};

function _getGdprConsent(bidderRequest) {
Expand All @@ -539,7 +588,7 @@ function _getGdprConsent(bidderRequest) {
const consent = {};

if (apiVersion !== undefined) {
consent.apiVersion = apiVersion
consent.apiVersion = apiVersion;
}

if (consentString !== undefined) {
Expand Down Expand Up @@ -575,10 +624,62 @@ function _getSchain(bidRequest) {

function _getEids(bidRequest) {
if (utils.deepAccess(bidRequest, 'userId')) {
return createEidsArray(bidRequest.userId)
return createEidsArray(bidRequest.userId);
}
}

function _buildVideoBidRequest(bidRequest) {
const videoAdUnitParams = utils.deepAccess(bidRequest, 'mediaTypes.video', {});
const videoBidderParams = utils.deepAccess(bidRequest, 'params.video', {});
const computedParams = {};

// Special case for playerSize.
// Eeach props will be overrided if they are defined in config.
if (Array.isArray(videoAdUnitParams.playerSize)) {
const tempSize = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize;
computedParams.w = tempSize[0];
computedParams.h = tempSize[1];
}

const videoParams = {
...computedParams,
...videoAdUnitParams,
...videoBidderParams
};

if (videoParams.context && videoParams.context === OUTSTREAM) {
bidRequest.mediaTypes.video.playerName = (internal.isRendererPreferredFromPublisher(bidRequest)) ? 'other' : 'adagio';

if (bidRequest.mediaTypes.video.playerName === 'other') {
utils.logWarn(`${LOG_PREFIX} renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.`);
}
}

// Only whitelisted OpenRTB options need to be validated.
// Other options will still remain in the `mediaTypes.video` object
// sent in the ad-request, but will be ignored by the SSP.
Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => {
if (videoParams.hasOwnProperty(paramName)) {
if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) {
bidRequest.mediaTypes.video[paramName] = videoParams[paramName];
} else {
delete bidRequest.mediaTypes.video[paramName];
utils.logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`);
}
}
});
}

function _renderer(bid) {
bid.renderer.push(() => {
if (typeof window.ADAGIO.outstreamPlayer === 'function') {
window.ADAGIO.outstreamPlayer(bid);
} else {
utils.logError(`${LOG_PREFIX} Adagio outstream player is not defined`);
}
});
}

export const spec = {
code: BIDDER_CODE,
gvlid: GVLID,
Expand All @@ -600,7 +701,7 @@ export const spec = {
...params,
adUnitElementId,
environment
}
};

const debugData = () => ({
action: 'pb-dbg',
Expand Down Expand Up @@ -631,7 +732,7 @@ export const spec = {
// Store adUnits config.
// If an adUnitCode has already been stored, it will be replaced.
w.ADAGIO = w.ADAGIO || {};
w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode)
w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode);
w.ADAGIO.pbjsAdUnits.push({
code: adUnitCode,
mediaTypes: mediaTypes || {},
Expand Down Expand Up @@ -667,14 +768,19 @@ export const spec = {
const eids = _getEids(validBidRequests[0]) || [];
const adUnits = utils._map(validBidRequests, (bidRequest) => {
bidRequest.features = internal.getFeatures(bidRequest, bidderRequest);

if (utils.deepAccess(bidRequest, 'mediaTypes.video')) {
_buildVideoBidRequest(bidRequest);
}

return bidRequest;
});

// Group ad units by organizationId
const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => {
adUnit.params.organizationId = adUnit.params.organizationId.toString();

groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || []
groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || [];
groupedAdUnits[adUnit.params.organizationId].push(adUnit);

return groupedAdUnits;
Expand Down Expand Up @@ -709,7 +815,7 @@ export const spec = {
options: {
contentType: 'text/plain'
}
}
};
});

return requests;
Expand All @@ -730,7 +836,30 @@ export const spec = {
if (response.bids) {
response.bids.forEach(bidObj => {
const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId));

if (bidReq) {
if (bidObj.mediaType === VIDEO) {
const mediaTypeContext = utils.deepAccess(bidReq, 'mediaTypes.video.context');
// Adagio SSP returns a `vastXml` only. No `vastUrl` nor `videoCacheKey`.
if (!bidObj.vastUrl && bidObj.vastXml) {
bidObj.vastUrl = 'data:text/xml;charset=utf-8;base64,' + btoa(bidObj.vastXml.replace(/\\"/g, '"'));
}

if (mediaTypeContext === OUTSTREAM) {
bidObj.renderer = Renderer.install({
id: bidObj.requestId,
adUnitCode: bidObj.adUnitCode,
url: bidObj.urlRenderer || RENDERER_URL,
config: {
...utils.deepAccess(bidReq, 'mediaTypes.video'),
...utils.deepAccess(bidObj, 'outstream', {})
}
});

bidObj.renderer.setRender(_renderer);
}
}

bidObj.site = bidReq.params.site;
bidObj.placement = bidReq.params.placement;
bidObj.pagetype = bidReq.params.pagetype;
Expand Down
13 changes: 13 additions & 0 deletions modules/adagioBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ Connects to Adagio demand source to fetch bids.
banner: {
sizes: [[300, 250], [300, 600]],
}
video: {
outstream: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this outstream: { is not needed. The correct format should be:

{
  code: "video1",
    mediaTypes: {
      video: {
        context: "outstream",
        playerSize: [640, 480],
        ...
      }
   }
}
``` 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, it works fine!
If you want to update the adagioBidAdapter.md file and fix the example (by removing the outstream: { line (and the corresponding } ) we can merge this one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @musikele

I amended the adagioBidAdapter.md, so you can merge when you want :)

context: 'outstream',
playerSize: [640, 480],
mimes: ['video/mp4'],
skip: 1
// Other OpenRTB 2.5 video options…
}
}
},
bids: [{
bidder: 'adagio', // Required
Expand All @@ -38,6 +47,10 @@ Connects to Adagio demand source to fetch bids.
category: 'sport', // Recommended. Category of the content displayed in the page.
subcategory: 'handball', // Optional. Subcategory of the content displayed in the page.
postBid: false // Optional. Use it in case of Post-bid integration only.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a , after postBid: false - discovered while copy/pasting to test the adapter.

video: {
skip: 0
// OpenRTB 2.5 video options defined here override ones defined in mediaTypes.
}
}
}]
}
Expand Down
Loading