From fadc303fe10a8d5327590da115a1b519f88779c4 Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Thu, 25 Apr 2024 09:47:53 -0300 Subject: [PATCH] JW Player Bid Adapter : initial release (#11344) * jwplayerBidAdapter module created * created initial skeleton of jwplayerBidAdapter * cleaned up initial skeleton for jwplayerBidAdapter * added description to jwplayerBidAdapter.md * created jwplayerBidAdapter unit test suite * implemented isBidRequestValid with unit tests * initial creation of the buildRequests function * incorporated feedback from pr and refactored. * added video object, impExt object, and site content object to jwplayerBidAdapter * added feedback from pr * fixed parameters for methods in jwplayerBidAdapter * added feedback from pr and unit tests * removed superfluous array * fixed nits * added feedback to jw adapter and tests for buildRequests * added feedback * one last feedback for request site * added site object unit test * fixed buildRequestSite object * added unit tests for site object * fixed unit tests for buildRequestSite * skeleton of interpretResponse * initial attempt at interpretResponse * started unit test * finished interpretResponse with unit test * populates schain as well as other params * reads schain from bidrequest * updates unit tests * improves tests * tests tmax * deletes obsolete comments * errors when url is missing * updates tests * implements usersync * supports iframe sync * registers user sync properly * tests user sync * uses boost email * improves ortb reqs * interprets response * updates tests * updates tests * removes playerSize official support * adds md * updates demo * adds content url * adds jwplayer ssp * renames demo --------- Co-authored-by: Jorge Rocha --- .../noadserver/jwplayerBidAdapter_sample.html | 75 ++++ .../jwplayerRtdProvider_example.html | 7 + .../videoModule/jwplayer/bidMarkedAsUsed.html | 7 + .../jwplayer/bidRequestScheduling.html | 13 +- .../jwplayer/bidsBackHandlerOverride.html | 7 + .../videoModule/jwplayer/eventListeners.html | 7 + .../videoModule/jwplayer/eventsUI.html | 7 + .../jwplayer/gamAdServerMediation.html | 7 + .../videoModule/jwplayer/mediaMetadata.html | 7 + .../videoModule/jwplayer/playlist.html | 7 + .../videoModule/videojs/bidMarkedAsUsed.html | 7 + .../videojs/bidRequestScheduling.html | 7 + .../videojs/bidsBackHandlerOverride.html | 7 + .../videoModule/videojs/eventListeners.html | 7 + .../videoModule/videojs/eventsUI.html | 7 + .../videojs/gamAdServerMediation.html | 7 + .../videoModule/videojs/mediaMetadata.html | 7 + .../videoModule/videojs/playlist.html | 7 + modules/jwplayerBidAdapter.js | 412 ++++++++++++++++++ modules/jwplayerBidAdapter.md | 72 +++ test/spec/modules/jwplayerBidAdapter_spec.js | 412 ++++++++++++++++++ 21 files changed, 1092 insertions(+), 4 deletions(-) create mode 100644 integrationExamples/noadserver/jwplayerBidAdapter_sample.html create mode 100644 modules/jwplayerBidAdapter.js create mode 100644 modules/jwplayerBidAdapter.md create mode 100644 test/spec/modules/jwplayerBidAdapter_spec.js diff --git a/integrationExamples/noadserver/jwplayerBidAdapter_sample.html b/integrationExamples/noadserver/jwplayerBidAdapter_sample.html new file mode 100644 index 00000000000..f8b15af64a2 --- /dev/null +++ b/integrationExamples/noadserver/jwplayerBidAdapter_sample.html @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/integrationExamples/realTimeData/jwplayerRtdProvider_example.html b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html index 4f29ef1c406..f3f0c64fb1a 100644 --- a/integrationExamples/realTimeData/jwplayerRtdProvider_example.html +++ b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html @@ -35,6 +35,13 @@ }, // Replace this object to test a new Adapter! bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html index 80ea81d09b6..d0b261043e4 100644 --- a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html index 663765317b0..c40af32cac2 100644 --- a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html +++ b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html @@ -50,12 +50,17 @@ mediationLayerAdServer: "dfp", bidTimeout: 2000 }, - bidders: [ - { + bidders: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { name: "ix", siteId: "300" - } - ] + }] } } } diff --git a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html index 66eaff26090..75a72ba3501 100644 --- a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html +++ b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html @@ -19,6 +19,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/eventListeners.html b/integrationExamples/videoModule/jwplayer/eventListeners.html index 39acb086107..6f04f37264b 100644 --- a/integrationExamples/videoModule/jwplayer/eventListeners.html +++ b/integrationExamples/videoModule/jwplayer/eventListeners.html @@ -23,6 +23,13 @@ // Replace this object to test a new Adapter! bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/eventsUI.html b/integrationExamples/videoModule/jwplayer/eventsUI.html index 78d72a6153d..cfd1efe7624 100644 --- a/integrationExamples/videoModule/jwplayer/eventsUI.html +++ b/integrationExamples/videoModule/jwplayer/eventsUI.html @@ -22,6 +22,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html index 018c8eba8b2..1f4331785ea 100644 --- a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html +++ b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html @@ -19,6 +19,13 @@ divId: 'player', // required to indicate which player is being used to render this ad unit. }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/mediaMetadata.html b/integrationExamples/videoModule/jwplayer/mediaMetadata.html index 7581af571d3..63e62aa4b82 100644 --- a/integrationExamples/videoModule/jwplayer/mediaMetadata.html +++ b/integrationExamples/videoModule/jwplayer/mediaMetadata.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/jwplayer/playlist.html b/integrationExamples/videoModule/jwplayer/playlist.html index 89efaea3d5c..9e89f606f23 100644 --- a/integrationExamples/videoModule/jwplayer/playlist.html +++ b/integrationExamples/videoModule/jwplayer/playlist.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html index d6656bc0c93..35745ab281f 100644 --- a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/bidRequestScheduling.html b/integrationExamples/videoModule/videojs/bidRequestScheduling.html index eb10fda89a2..da6499ca4cc 100644 --- a/integrationExamples/videoModule/videojs/bidRequestScheduling.html +++ b/integrationExamples/videoModule/videojs/bidRequestScheduling.html @@ -37,6 +37,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html index ac8f4163e76..74217ecee2c 100644 --- a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html +++ b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html @@ -34,6 +34,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/eventListeners.html b/integrationExamples/videoModule/videojs/eventListeners.html index 1966f134e02..3fc2ef7137c 100644 --- a/integrationExamples/videoModule/videojs/eventListeners.html +++ b/integrationExamples/videoModule/videojs/eventListeners.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/eventsUI.html b/integrationExamples/videoModule/videojs/eventsUI.html index 9eba09f7a52..215b2de4d25 100644 --- a/integrationExamples/videoModule/videojs/eventsUI.html +++ b/integrationExamples/videoModule/videojs/eventsUI.html @@ -37,6 +37,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/gamAdServerMediation.html b/integrationExamples/videoModule/videojs/gamAdServerMediation.html index 6ffc1a67c03..d6603abbf8f 100644 --- a/integrationExamples/videoModule/videojs/gamAdServerMediation.html +++ b/integrationExamples/videoModule/videojs/gamAdServerMediation.html @@ -34,6 +34,13 @@ divId: 'player', // required to indicate which player is being used to render this ad unit. }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/mediaMetadata.html b/integrationExamples/videoModule/videojs/mediaMetadata.html index ede076fd814..084c597cddd 100644 --- a/integrationExamples/videoModule/videojs/mediaMetadata.html +++ b/integrationExamples/videoModule/videojs/mediaMetadata.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/playlist.html b/integrationExamples/videoModule/videojs/playlist.html index eb813f095f7..2563717df41 100644 --- a/integrationExamples/videoModule/videojs/playlist.html +++ b/integrationExamples/videoModule/videojs/playlist.html @@ -36,6 +36,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/modules/jwplayerBidAdapter.js b/modules/jwplayerBidAdapter.js new file mode 100644 index 00000000000..151d08bf3a6 --- /dev/null +++ b/modules/jwplayerBidAdapter.js @@ -0,0 +1,412 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { isArray, isFn, deepAccess, deepSetValue, getDNT, logError, logWarn } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; + +const BIDDER_CODE = 'jwplayer'; +const BASE_URL = 'https://vpb-server.jwplayer.com/'; +const AUCTION_URL = BASE_URL + 'openrtb2/auction'; +const USER_SYNC_URL = BASE_URL + 'setuid'; +const GVLID = 1046; +const SUPPORTED_AD_TYPES = [VIDEO]; + +const VIDEO_ORTB_PARAMS = [ + 'pos', + 'w', + 'h', + 'playbackend', + 'mimes', + 'minduration', + 'maxduration', + 'protocols', + 'startdelay', + 'placement', + 'plcmt', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + +function getBidAdapter() { + function isBidRequestValid(bid) { + const params = bid && bid.params; + if (!params) { + return false; + } + + return !!params.placementId && !!params.publisherId && !!params.siteId; + } + + function buildRequests(bidRequests, bidderRequest) { + if (!bidRequests) { + return; + } + + if (!hasContentUrl(bidderRequest.ortb2)) { + logError(`${BIDDER_CODE}: cannot bid without a valid Content URL. Please populate ortb2.site.content.url`); + return; + } + + const warnings = getWarnings(bidderRequest); + warnings.forEach(warning => { + logWarn(`${BIDDER_CODE}: ${warning}`); + }); + + return bidRequests.map(bidRequest => { + const payload = buildRequest(bidRequest, bidderRequest); + + return { + method: 'POST', + url: AUCTION_URL, + data: payload + } + }); + } + + function interpretResponse(serverResponse) { + const outgoingBidResponses = []; + const serverResponseBody = serverResponse.body; + + logResponseWarnings(serverResponseBody); + + const seatBids = serverResponseBody && serverResponseBody.seatbid; + if (!isArray(seatBids)) { + return outgoingBidResponses; + } + + const cur = serverResponseBody.cur; + + seatBids.forEach(seatBid => { + seatBid.bid.forEach(bid => { + const bidResponse = { + requestId: serverResponseBody.id, + cpm: bid.price, + currency: cur, + width: bid.w, + height: bid.h, + ad: bid.adm, + vastXml: bid.adm, + ttl: bid.ttl || 3600, + netRevenue: false, + creativeId: bid.adid, + dealId: bid.dealid, + meta: { + advertiserDomains: bid.adomain, + mediaType: VIDEO, + primaryCatId: bid.cat, + } + }; + + outgoingBidResponses.push(bidResponse); + }); + }); + + return outgoingBidResponses; + } + + function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!hasPurpose1Consent(gdprConsent)) { + return []; + } + + const userSyncs = []; + const consentQueryParams = getUserSyncConsentQueryParams(gdprConsent); + const url = `https://ib.adnxs.com/getuid?${USER_SYNC_URL}?bidder=jwplayer&uid=$UID&f=i` + consentQueryParams + + if (syncOptions.iframeEnabled) { + userSyncs.push({ + type: 'iframe', + url + }); + } + + if (syncOptions.pixelEnabled) { + userSyncs.push({ + type: 'image', + url + }); + } + + return userSyncs; + } + + return { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs + } + + function getUserSyncConsentQueryParams(gdprConsent) { + if (!gdprConsent) { + return ''; + } + + const consentString = gdprConsent.consentString; + if (!consentString) { + return ''; + } + + let gdpr = 0; + const gdprApplies = gdprConsent.gdprApplies; + if (typeof gdprApplies === 'boolean') { + gdpr = Number(gdprApplies) + } + + return `&gdpr=${gdpr}&gdpr_consent=${consentString}`; + } + + function buildRequest(bidRequest, bidderRequest) { + const openrtbRequest = { + id: bidRequest.bidId, + imp: getRequestImpressions(bidRequest, bidderRequest), + site: getRequestSite(bidRequest, bidderRequest), + device: getRequestDevice(bidderRequest.ortb2), + user: getRequestUser(bidderRequest.ortb2), + }; + + // GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + if (bidRequest.schain) { + deepSetValue(openrtbRequest, 'source.schain', bidRequest.schain); + } + + openrtbRequest.tmax = bidderRequest.timeout || 200; + + return JSON.stringify(openrtbRequest); + } + + function getRequestImpressions(bidRequest) { + const impressionObject = { + id: bidRequest.adUnitCode, + }; + + impressionObject.video = getImpressionVideo(bidRequest); + + const bidFloorData = getBidFloorData(bidRequest); + if (bidFloorData) { + impressionObject.bidfloor = bidFloorData.floor; + impressionObject.bidfloorcur = bidFloorData.currency; + } + + impressionObject.ext = getImpressionExtension(bidRequest); + + return [impressionObject]; + } + + function getImpressionVideo(bidRequest) { + const videoParams = deepAccess(bidRequest, 'mediaTypes.video', {}); + + const video = {}; + + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + setPlayerSize(video, videoParams); + + if (!videoParams.plcmt) { + logWarn(`${BIDDER_CODE}: Please set a value to mediaTypes.video.plcmt`); + } + + return video; + } + + function getImpressionExtension(bidRequest) { + return { + prebid: { + bidder: { + jwplayer: { + placementId: bidRequest.params.placementId + } + } + } + }; + } + + function setPlayerSize(videoImp, videoParams) { + if (videoImp.w !== undefined && videoImp.h !== undefined) { + return; + } + + const playerSize = getNormalizedPlayerSize(videoParams.playerSize); + if (!playerSize.length) { + logWarn(logWarn(`${BIDDER_CODE}: Video size has not been set. Please set values in video.h and video.w`)); + return; + } + + if (videoImp.w === undefined) { + videoImp.w = playerSize[0]; + } + + if (videoImp.h === undefined) { + videoImp.h = playerSize[1]; + } + } + + function getNormalizedPlayerSize(playerSize) { + if (!Array.isArray(playerSize)) { + return []; + } + + if (Array.isArray(playerSize[0])) { + playerSize = playerSize[0]; + } + + if (playerSize.length < 2) { + return []; + } + + return playerSize; + } + + function getBidFloorData(bidRequest) { + const { params } = bidRequest; + const currency = params.currency || 'USD'; + + let floorData; + if (isFn(bidRequest.getFloor)) { + const bidFloorRequest = { + currency: currency, + mediaType: VIDEO, + size: '*' + }; + floorData = bidRequest.getFloor(bidFloorRequest); + } else if (params.bidFloor) { + floorData = { floor: params.bidFloor, currency: currency }; + } + + return floorData; + } + + function getRequestSite(bidRequest, bidderRequest) { + const site = bidderRequest.ortb2.site || {}; + + site.domain = site.domain || config.publisherDomain || window.location.hostname; + site.page = site.page || config.pageUrl || window.location.href; + + const referer = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + if (!site.ref && referer) { + site.ref = referer; + } + + const jwplayerPublisherExtChain = 'publisher.ext.jwplayer.'; + + deepSetValue(site, jwplayerPublisherExtChain + 'publisherId', bidRequest.params.publisherId); + deepSetValue(site, jwplayerPublisherExtChain + 'siteId', bidRequest.params.siteId); + + return site; + } + + function getRequestDevice(ortb2) { + const device = Object.assign({ + h: screen.height, + w: screen.width, + ua: navigator.userAgent, + dnt: getDNT() ? 1 : 0, + js: 1 + }, ortb2.device || {}) + + const language = getLanguage(); + if (!device.language && language) { + device.language = language; + } + + return device; + } + + function getLanguage() { + const navigatorLanguage = navigator.language; + if (!navigatorLanguage) { + return; + } + + const languageCodeSegments = navigatorLanguage.split('-'); + if (!languageCodeSegments.length) { + return; + } + + return languageCodeSegments[0]; + } + + function getRequestUser(ortb2) { + const user = ortb2.user || {}; + if (config.getConfig('coppa') === true) { + user.coppa = true; + } + + return user; + } + + function hasContentUrl(ortb2) { + const site = ortb2.site; + const content = site && site.content; + return !!(content && content.url); + } + + function getWarnings(bidderRequest) { + const content = bidderRequest.ortb2.site.content; + const contentChain = 'ortb2.site.content.'; + const warnings = []; + if (!content.id) { + warnings.push(getMissingFieldMessage(contentChain + 'id')); + } + + if (!content.title) { + warnings.push(getMissingFieldMessage(contentChain + 'title')); + } + + if (!content.ext || !content.ext.description) { + warnings.push(getMissingFieldMessage(contentChain + 'ext.description')); + } + + return warnings; + } + + function getMissingFieldMessage(fieldName) { + return `Optional field ${fieldName} is not populated; we recommend populating for maximum performance.` + } + + function logResponseWarnings(serverResponseBody) { + const warningPayload = deepAccess(serverResponseBody, 'ext.warnings'); + if (!warningPayload) { + return; + } + + const warningCategories = Object.keys(warningPayload); + warningCategories.forEach(category => { + const warnings = warningPayload[category]; + if (!isArray(warnings)) { + return; + } + + warnings.forEach(warning => { + logWarn(`${BIDDER_CODE}: [Bid Response][Warning Code: ${warning.code}] ${warning.message}`); + }); + }); + } +} + +export const spec = getBidAdapter(); + +registerBidder(spec); diff --git a/modules/jwplayerBidAdapter.md b/modules/jwplayerBidAdapter.md new file mode 100644 index 00000000000..620f8657e50 --- /dev/null +++ b/modules/jwplayerBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: JWPlayer Bid Adapter +Module Type: Bidder Adapter +Maintainer: boost-engineering@jwplayer.com +``` + +# Description + +Connects to JWPlayer's demand sources. + +JWPlayer bid adapter supports Video (instream and outstream). + +# Sample Ad Unit + +```markdown +const adUnit = { + code: 'test-ad-unit', + mediaTypes: { + video: { + pos: 0, + w: 640, + h: 480, + mimes : ['video/x-ms-wmv', 'video/mp4'], + minduration : 0, + maxduration: 60, + protocols : [2,3,7,5,6,8], + startdelay: 0, + placement: 1, + plcmt: 1, + skip: 1, + skipafter: 10, + playbackmethod: [3], + api: [2], + linearity: 1 + } + }, + bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }] +}; +``` + +# Sample ortb2 config + +```markdown +pbjs.setConfig({ + ortb2: { + site: { + publisher: { + id: 'test-publisher-id' + }, + content: { + id: 'test-media-id', + url: 'test.mp4', + title: 'title of my media', + ext: { + description: 'description of my media' + } + }, + domain: 'test-domain.com', + page: 'https://www.test-domain.com/test.html', + } + } +} +``` diff --git a/test/spec/modules/jwplayerBidAdapter_spec.js b/test/spec/modules/jwplayerBidAdapter_spec.js new file mode 100644 index 00000000000..e19790a9670 --- /dev/null +++ b/test/spec/modules/jwplayerBidAdapter_spec.js @@ -0,0 +1,412 @@ +import { expect, assert } from 'chai'; +import { spec } from 'modules/jwplayerBidAdapter.js'; + +describe('jwplayerBidAdapter', function() { + beforeEach(function() { + this.defaultBidderRequest = { + gdprConsent: { + consentString: 'testConsentString', + gdprApplies: true + }, + uspConsent: 'testCCPA', + refererInfo: { + referer: 'https://example.com' + }, + ortb2: { + site: { + domain: 'page.example.com', + page: 'https://examplepage.com', + content: { + url: 'media.mp4', + id: 'testMediaId', + title: 'testTile', + data: [{ + name: 'jwplayer.com', + segment: [{ + id: '00000000' + }, { + id: '88888888' + }, { + id: '80808080' + }], + ext: { + segtax: 502, + cids: ['testMediaId', 'externalTestId'], + } + }], + ext: { + description: 'testDescription' + } + } + } + }, + timeout: 1000 + } + }); + + it('should use jwplayer bidder code', function () { + expect(spec.code).to.equal('jwplayer'); + }); + + it('should use jwplayer GVLID code', function () { + expect(spec.gvlid).to.equal(1046); + }); + + it('should support VIDEO media type only', function () { + expect(spec.supportedMediaTypes).to.have.length(1); + expect(spec.supportedMediaTypes[0]).to.equal('video'); + }); + + describe('isBidRequestValid', function() { + it('should be invalid when bidRequest is undefined', function() { + assert(spec.isBidRequestValid() === false); + }); + + it('should be invalid when bidRequest is null', function() { + assert(spec.isBidRequestValid(null) === false); + }); + + it('should be invalid when the bidRequest has no params', function() { + assert(spec.isBidRequestValid({}) === false); + }); + + it('should be invalid when the bid request only includes a publisher ID', function() { + assert(spec.isBidRequestValid({params: {publisherId: 'foo'}}) === false); + }); + + it('should be invalid when the bid request only includes a placement ID', function() { + assert(spec.isBidRequestValid({params: {placementId: 'foo'}}) === false); + }); + + it('should be invalid when the bid request only includes a site ID', function() { + assert(spec.isBidRequestValid({params: {siteId: 'foo'}}) === false); + }); + + it('should be valid when the bid includes a placement ID, a publisher ID and a site ID', function() { + assert(spec.isBidRequestValid({params: {placementId: 'foo', publisherId: 'bar', siteId: 'siteId '}}) === true); + }); + }); + + describe('buildRequests', function() { + it('should return undefined when bidRequests is undefined', function () { + expect(spec.buildRequests()).to.be.undefined; + }); + + it('should return undefined when bidRequests is null', function () { + expect(spec.buildRequests(null)).to.be.undefined; + }); + + it('should return undefined when ortb site.content.url is absent', function () { + const request = spec.buildRequests({}, { + ortb2: { + site: { + content: { + url: null, + } + } + } + }); + + expect(request).to.be.undefined; + }); + + it('should build a valid request when bid request is complete', function() { + const incomingBidRequests = [ + { + bidder: 'jwplayer', + params: { + placementId: 'testPlacementId', + publisherId: 'testPublisherId', + siteId: 'testSiteId', + bidFloor: 10, + currency: 'EUR', + }, + mediaTypes: { + video: { + pos: 3, + w: 640, + h: 480, + context: 'instream', + mimes: [ + 'video/mp4', + 'application/javascript' + ], + protocols: [2, 3, 5, 6], + maxduration: 60, + minduration: 3, + startdelay: 0, + linearity: 1, + placement: 1, + plcmt: 1, + skip: 1, + skipafter: 4, + minbitrate: 500, + maxbitrate: 1000, + api: [2], + delivery: [2], + playbackmethod: [1], + playbackend: 2 + } + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'publisher.com', + sid: '00001', + hp: 1 + } + ] + }, + bidRequestsCount: 1, + adUnitCode: 'testAdUnitCode', + bidId: 'testBidId' + } + ]; + + const outgoingBidRequests = spec.buildRequests(incomingBidRequests, this.defaultBidderRequest); + + outgoingBidRequests.forEach(serverRequest => { + expect(serverRequest.url).to.equal('https://vpb-server.jwplayer.com/openrtb2/auction'); + expect(serverRequest.method).to.equal('POST'); + + const openrtbRequest = JSON.parse(serverRequest.data); + + expect(openrtbRequest.id).to.equal('testBidId'); + + expect(openrtbRequest.imp[0].id).to.equal('testAdUnitCode'); + expect(openrtbRequest.imp[0].video.w).to.equal(640); + expect(openrtbRequest.imp[0].video.h).to.equal(480); + expect(openrtbRequest.imp[0].video.mimes).to.deep.equal(['video/mp4', 'application/javascript']); + expect(openrtbRequest.imp[0].video.protocols).to.deep.equal([2, 3, 5, 6]); + expect(openrtbRequest.imp[0].video.api).to.deep.equal([2]); + expect(openrtbRequest.imp[0].video.startdelay).to.equal(0); + expect(openrtbRequest.imp[0].video.placement).to.equal(1); + expect(openrtbRequest.imp[0].video.plcmt).to.equal(1); + expect(openrtbRequest.imp[0].video.pos).to.equal(3); + expect(openrtbRequest.imp[0].video.minduration).to.equal(3); + expect(openrtbRequest.imp[0].video.maxduration).to.equal(60); + expect(openrtbRequest.imp[0].video.skip).to.equal(1); + expect(openrtbRequest.imp[0].video.skipafter).to.equal(4); + expect(openrtbRequest.imp[0].video.minbitrate).to.equal(500); + expect(openrtbRequest.imp[0].video.maxbitrate).to.equal(1000); + expect(openrtbRequest.imp[0].video.delivery).to.deep.equal([2]); + expect(openrtbRequest.imp[0].video.playbackmethod).to.deep.equal([1]); + expect(openrtbRequest.imp[0].video.playbackend).to.equal(2); + expect(openrtbRequest.imp[0].video.linearity).to.equal(1); + + expect(openrtbRequest.imp[0].bidfloor).to.equal(10); + expect(openrtbRequest.imp[0].bidfloorcur).to.equal('EUR'); + + expect(openrtbRequest.imp[0].ext.prebid.bidder.jwplayer.placementId).to.equal('testPlacementId'); + + expect(openrtbRequest.site.domain).to.equal('page.example.com'); + expect(openrtbRequest.site.page).to.equal('https://examplepage.com'); + expect(openrtbRequest.site.ref).to.equal('https://example.com'); + + expect(openrtbRequest.site.publisher.ext.jwplayer.publisherId).to.equal('testPublisherId'); + expect(openrtbRequest.site.publisher.ext.jwplayer.siteId).to.equal('testSiteId'); + + expect(openrtbRequest.site.content.url).to.equal('media.mp4'); + expect(openrtbRequest.site.content.id).to.equal('testMediaId'); + expect(openrtbRequest.site.content.title).to.equal('testTile'); + expect(openrtbRequest.site.content.ext.description).to.equal('testDescription'); + expect(openrtbRequest.site.content.data.length).to.equal(1); + const datum = openrtbRequest.site.content.data[0]; + expect(datum.name).to.equal('jwplayer.com'); + expect(datum.segment).to.deep.equal([{ + id: '00000000' + }, { + id: '88888888' + }, { + id: '80808080' + }]); + expect(datum.ext.segtax).to.equal(502); + expect(datum.ext.cids).to.deep.equal(['testMediaId', 'externalTestId']); + + expect(openrtbRequest.device.ua).to.equal(navigator.userAgent); + expect(openrtbRequest.device.w).to.not.be.undefined; + expect(openrtbRequest.device.h).to.not.be.undefined; + expect(openrtbRequest.device.dnt).to.not.be.undefined; + expect(openrtbRequest.device.js).to.equal(1); + expect(openrtbRequest.device.language).to.not.be.undefined; + + expect(openrtbRequest.user.ext.consent).to.equal('testConsentString'); + + expect(openrtbRequest.regs.ext.gdpr).to.equal(1); + expect(openrtbRequest.regs.ext.us_privacy).to.equal('testCCPA'); + + expect(openrtbRequest.source.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'publisher.com', + sid: '00001', + hp: 1 + } + ] + }); + + expect(openrtbRequest.tmax).to.equal(1000); + }); + }); + }); + + describe('interpretResponse', function() { + const serverResponse = { + body: { + id: 'test-request-id', + cur: 'USD', + seatbid: [{ + bid: [{ + id: 'testId', + impid: 'test-imp-id', + price: 5.000, + adid: 'test-creative-id', + adm: 'test-ad-xml', + adomain: ['prebid.com'], + cat: ['test-iab-category'], + w: 200, + h: 150, + dealid: 'test-deal-id' + }], + seat: 1000 + }] + } + }; + + const bidResponses = spec.interpretResponse(serverResponse); + + expect(bidResponses).to.have.length(1); + const bid = bidResponses[0]; + expect(bid.requestId).to.equal('test-request-id'); + expect(bid.cpm).to.equal(5); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(200); + expect(bid.height).to.equal(150); + expect(bid.creativeId).to.equal('test-creative-id'); + expect(bid.vastXml).to.equal('test-ad-xml'); + expect(bid.netRevenue).to.equal(false); + expect(bid.ttl).to.equal(3600); + expect(bid.ad).to.equal('test-ad-xml'); + expect(bid.dealId).to.equal('test-deal-id'); + + expect(bid.meta).to.not.be.undefined; + + expect(bid.meta.advertiserDomains).to.have.length(1); + expect(bid.meta.advertiserDomains[0]).to.equal('prebid.com'); + + expect(bid.meta.mediaType).to.equal('video'); + + expect(bid.meta.primaryCatId).to.have.length(1); + expect(bid.meta.primaryCatId[0]).to.equal('test-iab-category'); + }); + + describe('getUserSyncs', function() { + const consentString = 'test_consent_string'; + const baseGdprConsent = { + gdprApplies: true, + vendorData: { + purpose: { + consents: { + 1: true + } + } + } + }; + + const expectedBaseUrl = 'https://ib.adnxs.com/getuid?https://vpb-server.jwplayer.com/setuid?bidder=jwplayer&uid=$UID&f=i'; + + it('should return empty when Purpose 1 consent is not granted', function() { + expect(spec.getUserSyncs({}, {})).to.be.empty; + expect(spec.getUserSyncs({}, {}, {})).to.be.empty; + expect(spec.getUserSyncs({}, {}, { gdprApplies: false })).to.be.empty; + expect(spec.getUserSyncs({}, {}, { + gdprApplies: true, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + })).to.be.empty; + }); + + it('should return iframe when enabled', function () { + const userSyncs = spec.getUserSyncs({ iframeEnabled: true }, {}, baseGdprConsent); + expect(userSyncs.length).to.equal(1); + const sync = userSyncs[0]; + expect(sync.type).to.equal('iframe'); + expect(sync.url).to.equal(expectedBaseUrl); + }); + + it('should return image when enabled', function () { + const userSyncs = spec.getUserSyncs({ pixelEnabled: true }, {}, baseGdprConsent); + expect(userSyncs.length).to.equal(1); + const sync = userSyncs[0]; + expect(sync.type).to.equal('image'); + expect(sync.url).to.equal(expectedBaseUrl); + }); + + it('should return both iframe and image when enabled', function () { + const userSyncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, {}, baseGdprConsent); + expect(userSyncs.length).to.equal(2); + + const iframeSync = userSyncs[0]; + expect(iframeSync.type).to.equal('iframe'); + expect(iframeSync.url).to.equal(expectedBaseUrl); + + const imageSync = userSyncs[1]; + expect(imageSync.type).to.equal('image'); + expect(imageSync.url).to.equal(expectedBaseUrl); + }); + + it('should include gdpr consent query params in sync redirect url', function () { + const expectedUrl = expectedBaseUrl + '&gdpr=1&gdpr_consent=' + consentString; + const gdprConsent = Object.assign({ }, baseGdprConsent, { consentString }); + const userSyncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, {}, gdprConsent); + expect(userSyncs.length).to.equal(2); + + const iframeSync = userSyncs[0]; + expect(iframeSync.type).to.equal('iframe'); + expect(iframeSync.url).to.equal(expectedUrl); + + const imageSync = userSyncs[1]; + expect(imageSync.type).to.equal('image'); + expect(imageSync.url).to.equal(expectedUrl); + }); + + it('should include gdpr 0 in consent query params when gdprApplies is false', function () { + const expectedUrl = expectedBaseUrl + '&gdpr=0&gdpr_consent=' + consentString; + const gdprConsent = Object.assign({ }, baseGdprConsent, { consentString, gdprApplies: false }); + const userSyncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, {}, gdprConsent); + expect(userSyncs.length).to.equal(2); + + const iframeSync = userSyncs[0]; + expect(iframeSync.type).to.equal('iframe'); + expect(iframeSync.url).to.equal(expectedUrl); + + const imageSync = userSyncs[1]; + expect(imageSync.type).to.equal('image'); + expect(imageSync.url).to.equal(expectedUrl); + }); + + it('should include gdpr 0 in consent query params when gdprApplies is not a bool', function () { + const expectedUrl = expectedBaseUrl + '&gdpr=0&gdpr_consent=' + consentString; + const gdprConsent = Object.assign({ }, baseGdprConsent, { consentString, gdprApplies: 1 }); + const userSyncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, {}, gdprConsent); + expect(userSyncs.length).to.equal(2); + + const iframeSync = userSyncs[0]; + expect(iframeSync.type).to.equal('iframe'); + expect(iframeSync.url).to.equal(expectedUrl); + + const imageSync = userSyncs[1]; + expect(imageSync.type).to.equal('image'); + expect(imageSync.url).to.equal(expectedUrl); + }); + }); +});