From ee9e1304573f8efb7efe8a274e2f47007e39bff6 Mon Sep 17 00:00:00 2001 From: Mark Kuhar Date: Fri, 6 Jan 2023 16:37:03 +0100 Subject: [PATCH 1/2] add video support --- modules/outbrainBidAdapter.js | 84 ++++++++- test/spec/modules/outbrainBidAdapter_spec.js | 172 +++++++++++++++++-- 2 files changed, 233 insertions(+), 23 deletions(-) diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 3db1da0d689..07b959c3d02 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -4,11 +4,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import { NATIVE, BANNER, VIDEO } from '../src/mediaTypes.js'; +import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { deepAccess, deepSetValue, replaceAuctionPrice, _map, isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'outbrain'; const GVLID = 164; @@ -22,11 +24,12 @@ const NATIVE_PARAMS = { body: { id: 4, name: 'data', type: 2 }, cta: { id: 1, type: 12, name: 'data' } }; +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [ NATIVE, BANNER ], + supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], isBidRequestValid: (bid) => { if (typeof bid.params !== 'object') { return false; @@ -50,7 +53,7 @@ export const spec = { return ( !!config.getConfig('outbrain.bidderUrl') && - !!(bid.nativeParams || bid.sizes) + !!(bid.nativeParams || bid.sizes || deepAccess(bid, 'mediaTypes.video')) ); }, buildRequests: (validBidRequests, bidderRequest) => { @@ -85,6 +88,8 @@ export const spec = { assets: getNativeAssets(bid) }) } + } else if (isVideoRequest(bid)) { + imp.video = getVideoAsset(bid); } else { imp.banner = { format: transformSizes(bid.sizes) @@ -163,7 +168,12 @@ export const spec = { return bids.map((bid, id) => { const bidResponse = bidResponses[id]; if (bidResponse) { - const type = bid.nativeParams ? NATIVE : BANNER; + let type = BANNER; + if (bid.nativeParams) { + type = NATIVE; + } else if (isVideoRequest(bid)) { + type = VIDEO; + } const bidObject = { requestId: bid.bidId, cpm: bidResponse.price, @@ -176,10 +186,16 @@ export const spec = { }; if (type === NATIVE) { bidObject.native = parseNative(bidResponse); - } else { + } else if (type === BANNER) { bidObject.ad = bidResponse.adm; bidObject.width = bidResponse.w; bidObject.height = bidResponse.h; + } else if (type === VIDEO) { + bidObject.vastXml = bidResponse.adm; + const videoContext = deepAccess(bid, 'mediaTypes.video.context'); + if (videoContext === OUTSTREAM) { + bidObject.renderer = createRenderer(bid); + } } bidObject.meta = {}; if (bidResponse.adomain && bidResponse.adomain.length > 0) { @@ -304,6 +320,21 @@ function getNativeAssets(bid) { }).filter(Boolean); } +function getVideoAsset(bid) { + const sizes = flatten(bid.mediaTypes.video.playerSize); + return { + w: parseInt(sizes[0], 10), + h: parseInt(sizes[1], 10), + protocols: bid.mediaTypes.video.protocols, + playbackmethod: bid.mediaTypes.video.playbackmethod, + mimes: bid.mediaTypes.video.mimes, + skip: bid.mediaTypes.video.skip, + delivery: bid.mediaTypes.video.delivery, + api: bid.mediaTypes.video.api, + maxbitrate: bid.mediaTypes.video.maxbitrate + }; +} + /* Turn bid request sizes into ut-compatible format */ function transformSizes(requestSizes) { if (!isArray(requestSizes)) { @@ -338,3 +369,46 @@ function _getFloor(bid, type) { } return null; } + +function isVideoRequest(bid) { + return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); +} + +function createRenderer(bid) { + let config = {}; + let playerUrl = OUTSTREAM_RENDERER_URL; + let render = function (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: bid.sizes, + targetId: bid.adUnitCode, + adResponse: { content: bid.vastXml } + }); + }); + }; + + let externalRenderer = deepAccess(bid, 'mediaTypes.video.renderer'); + if (!externalRenderer) { + externalRenderer = deepAccess(bid, 'renderer'); + } + + if (externalRenderer) { + config = externalRenderer.options; + playerUrl = externalRenderer.url; + render = externalRenderer.render; + } + + const renderer = Renderer.install({ + id: bid.adUnitCode, + url: playerUrl, + config: config, + adUnitCode: bid.adUnitCode, + loaded: false + }); + try { + renderer.setRender(render); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} \ No newline at end of file diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index 5d7bebc1de1..5818ed873a1 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,7 +1,7 @@ -import {expect} from 'chai'; -import {spec} from 'modules/outbrainBidAdapter.js'; -import {config} from 'src/config.js'; -import {server} from 'test/mocks/xhr'; +import { expect } from 'chai'; +import { spec } from 'modules/outbrainBidAdapter.js'; +import { config } from 'src/config.js'; +import { server } from 'test/mocks/xhr'; import { createEidsArray } from 'modules/userId/eids.js'; describe('Outbrain Adapter', function () { @@ -45,6 +45,21 @@ describe('Outbrain Adapter', function () { ] } + const videoBidRequestParams = { + mediaTypes: { + video: { + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [1], + skip: 1, + placement: 4, + api: [2], + maxbitrate: 3000 + } + } + } + describe('isBidRequestValid', function () { before(() => { config.setConfig({ @@ -93,6 +108,18 @@ describe('Outbrain Adapter', function () { } expect(spec.isBidRequestValid(bid)).to.equal(true) }) + it('should succeed when bid contains video', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...videoBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) it('should fail if publisher id is not set', function () { const bid = { bidder: 'outbrain', @@ -298,6 +325,55 @@ describe('Outbrain Adapter', function () { expect(res.data).to.deep.equal(JSON.stringify(expectedData)) }) + it('should build video request', function () { + const bidRequest = { + ...commonBidRequest, + ...videoBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + video: { + w: 640, + h: 480, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [1], + mimes: ['video/mp4'], + skip: 1, + api: [2], + maxbitrate: 3000 + } + } + ], + ext: { + prebid: { + channel: { + name: 'pbjs', version: '$prebid.version$' + } + } + } + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + it('should pass optional parameters in request', function () { const bidRequest = { ...commonBidRequest, @@ -390,7 +466,7 @@ describe('Outbrain Adapter', function () { ...commonBidRequest, ...nativeBidRequestParams, } - config.setConfig({coppa: true}) + config.setConfig({ coppa: true }) const res = spec.buildRequests([bidRequest], commonBidderRequest) const resData = JSON.parse(res.data) @@ -412,7 +488,7 @@ describe('Outbrain Adapter', function () { let res = spec.buildRequests([bidRequest], commonBidderRequest); const resData = JSON.parse(res.data) expect(resData.user.ext.eids).to.deep.equal([ - {source: 'liveramp.com', uids: [{id: 'id-value', atype: 3}]} + { source: 'liveramp.com', uids: [{ id: 'id-value', atype: 3 }] } ]); }); @@ -421,7 +497,7 @@ describe('Outbrain Adapter', function () { ...commonBidRequest, ...nativeBidRequestParams, } - bidRequest.getFloor = function() { + bidRequest.getFloor = function () { return { currency: 'USD', floor: 1.23, @@ -619,6 +695,66 @@ describe('Outbrain Adapter', function () { const res = spec.interpretResponse(serverResponse, request) expect(res).to.deep.equal(expectedRes) }); + + it('should interpret video response', function () { + const serverResponse = { + body: { + id: '123', + seatbid: [ + { + bid: [ + { + id: '111', + impid: '1', + price: 1.1, + adm: '\u003cVAST version="3.0"\u003e\u003cAd\u003e\u003cInLine\u003e\u003cAdSystem\u003ezemanta\u003c/AdSystem\u003e\u003cAdTitle\u003e1\u003c/AdTitle\u003e\u003cImpression\u003ehttp://win.com\u003c/Impression\u003e\u003cImpression\u003ehttp://example.com/imptracker\u003c/Impression\u003e\u003cCreatives\u003e\u003cCreative\u003e\u003cLinear\u003e\u003cDuration\u003e00:00:25\u003c/Duration\u003e\u003cTrackingEvents\u003e\u003cTracking event="start"\u003ehttp://example.com/start\u003c/Tracking\u003e\u003cTracking event="progress" offset="00:00:03"\u003ehttp://example.com/p3s\u003c/Tracking\u003e\u003c/TrackingEvents\u003e\u003cVideoClicks\u003e\u003cClickThrough\u003ehttp://link.com\u003c/ClickThrough\u003e\u003c/VideoClicks\u003e\u003cMediaFiles\u003e\u003cMediaFile delivery="progressive" type="video/mp4" bitrate="700" width="640" height="360"\u003ehttps://example.com/123_360p.mp4\u003c/MediaFile\u003e\u003c/MediaFiles\u003e\u003c/Linear\u003e\u003c/Creative\u003e\u003c/Creatives\u003e\u003c/InLine\u003e\u003c/Ad\u003e\u003c/VAST\u003e', + adid: '100', + cid: '5', + crid: '29998660', + cat: ['cat-1'], + adomain: [ + 'example.com' + ] + } + ], + seat: '100', + group: 1 + } + ], + bidid: '456', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...videoBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'video', + nurl: undefined, + vastXml: 'zemanta1http://win.comhttp://example.com/imptracker00:00:25http://example.com/starthttp://example.com/p3shttp://link.comhttps://example.com/123_360p.mp4', + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); }) }) @@ -637,41 +773,41 @@ describe('Outbrain Adapter', function () { }) it('should return user sync if pixel enabled with outbrain config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + const ret = spec.getUserSyncs({ pixelEnabled: false }) expect(ret).to.be.an('array').that.is.empty }) it('should not return user sync if url is not set', function () { config.resetConfig() - const ret = spec.getUserSyncs({pixelEnabled: true}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); From a971f0d74e04a6abec6aa33e11b456776841c55a Mon Sep 17 00:00:00 2001 From: Mark Kuhar Date: Mon, 9 Jan 2023 17:37:06 +0100 Subject: [PATCH 2/2] add more video props --- modules/outbrainBidAdapter.js | 33 ++++++++++++++--- test/spec/modules/outbrainBidAdapter_spec.js | 38 +++++++++++++++++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 07b959c3d02..6bcbc6a1cba 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -5,8 +5,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { NATIVE, BANNER, VIDEO } from '../src/mediaTypes.js'; -import { INSTREAM, OUTSTREAM } from '../src/video.js'; -import { deepAccess, deepSetValue, replaceAuctionPrice, _map, isArray } from '../src/utils.js'; +import { OUTSTREAM } from '../src/video.js'; +import { deepAccess, deepSetValue, replaceAuctionPrice, _map, isArray, logWarn } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -53,7 +53,7 @@ export const spec = { return ( !!config.getConfig('outbrain.bidderUrl') && - !!(bid.nativeParams || bid.sizes || deepAccess(bid, 'mediaTypes.video')) + (!!(bid.nativeParams || bid.sizes) || isValidVideoRequest(bid)) ); }, buildRequests: (validBidRequests, bidderRequest) => { @@ -331,7 +331,13 @@ function getVideoAsset(bid) { skip: bid.mediaTypes.video.skip, delivery: bid.mediaTypes.video.delivery, api: bid.mediaTypes.video.api, - maxbitrate: bid.mediaTypes.video.maxbitrate + minbitrate: bid.mediaTypes.video.minbitrate, + maxbitrate: bid.mediaTypes.video.maxbitrate, + minduration: bid.mediaTypes.video.minduration, + maxduration: bid.mediaTypes.video.maxduration, + startdelay: bid.mediaTypes.video.startdelay, + placement: bid.mediaTypes.video.placement, + linearity: bid.mediaTypes.video.linearity }; } @@ -411,4 +417,21 @@ function createRenderer(bid) { logWarn('Prebid Error calling setRender on renderer', err); } return renderer; -} \ No newline at end of file +} + +function isValidVideoRequest(bid) { + const videoAdUnit = deepAccess(bid, 'mediaTypes.video') + if (!videoAdUnit) { + return false; + } + + if (!Array.isArray(videoAdUnit.playerSize)) { + return false; + } + + if (videoAdUnit.context == '') { + return false; + } + + return true; +} diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index 5818ed873a1..f5ce00ed8df 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -53,9 +53,14 @@ describe('Outbrain Adapter', function () { protocols: [1, 2, 3, 4, 5, 6, 7, 8], playbackmethod: [1], skip: 1, - placement: 4, api: [2], - maxbitrate: 3000 + minbitrate: 1000, + maxbitrate: 3000, + minduration: 3, + maxduration: 10, + startdelay: 2, + placement: 4, + linearity: 1 } } } @@ -120,6 +125,22 @@ describe('Outbrain Adapter', function () { } expect(spec.isBidRequestValid(bid)).to.equal(true) }) + it('should fail when bid contains insufficient video information', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) it('should fail if publisher id is not set', function () { const bid = { bidder: 'outbrain', @@ -357,7 +378,13 @@ describe('Outbrain Adapter', function () { mimes: ['video/mp4'], skip: 1, api: [2], - maxbitrate: 3000 + minbitrate: 1000, + maxbitrate: 3000, + minduration: 3, + maxduration: 10, + startdelay: 2, + placement: 4, + linearity: 1 } } ], @@ -714,7 +741,8 @@ describe('Outbrain Adapter', function () { cat: ['cat-1'], adomain: [ 'example.com' - ] + ], + nurl: 'http://example.com/win/${AUCTION_PRICE}' } ], seat: '100', @@ -742,7 +770,7 @@ describe('Outbrain Adapter', function () { netRevenue: false, currency: 'USD', mediaType: 'video', - nurl: undefined, + nurl: 'http://example.com/win/${AUCTION_PRICE}', vastXml: 'zemanta1http://win.comhttp://example.com/imptracker00:00:25http://example.com/starthttp://example.com/p3shttp://link.comhttps://example.com/123_360p.mp4', meta: { 'advertiserDomains': [