forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathadpod.js
656 lines (599 loc) · 26.2 KB
/
adpod.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
/**
* This module houses the functionality to evaluate and process adpod adunits/bids. Specifically there are several hooked functions,
* that either supplement the base function (ie to check something additional or unique to adpod objects) or to replace the base function
* entirely when appropriate.
*
* Brief outline of each hook:
* - `callPrebidCacheHook` - for any adpod bids, this function will temporarily hold them in a queue in order to send the bids to Prebid Cache in bulk
* - `checkAdUnitSetupHook` - evaluates the adUnits to ensure that required fields for adpod adUnits are present. Invalid adpod adUntis are removed from the array.
* - `checkVideoBidSetupHook` - evaluates the adpod bid returned from an adaptor/bidder to ensure required fields are populated; also initializes duration bucket field.
*
* To initialize the module, there is an `initAdpodHooks()` function that should be imported and executed by a corresponding `...AdServerVideo`
* module that designed to support adpod video type ads. This import process allows this module to effectively act as a sub-module.
*/
import {
deepAccess,
generateUUID,
groupBy,
isArray,
isArrayOfNums,
isNumber,
isPlainObject,
logError,
logInfo,
logWarn
} from '../src/utils.js';
import {
addBidToAuction,
AUCTION_IN_PROGRESS,
getPriceByGranularity,
getPriceGranularity
} from '../src/auction.js';
import {checkAdUnitSetup} from '../src/prebid.js';
import {checkVideoBidSetup} from '../src/video.js';
import {getHook, module, setupBeforeHookFnOnce} from '../src/hook.js';
import {store} from '../src/videoCache.js';
import {config} from '../src/config.js';
import {ADPOD} from '../src/mediaTypes.js';
import {find, arrayFrom as from} from '../src/polyfill.js';
import {auctionManager} from '../src/auctionManager.js';
import { TARGETING_KEYS } from '../src/constants.js';
const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur';
const TARGETING_KEY_CACHE_ID = 'hb_cache_id';
let queueTimeDelay = 50;
let queueSizeLimit = 5;
let bidCacheRegistry = createBidCacheRegistry();
/**
* Create a registry object that stores/manages bids while be held in queue for Prebid Cache.
* @returns registry object with defined accessor functions
*/
function createBidCacheRegistry() {
let registry = {};
function setupRegistrySlot(auctionId) {
registry[auctionId] = {};
registry[auctionId].bidStorage = new Set();
registry[auctionId].queueDispatcher = createDispatcher(queueTimeDelay);
registry[auctionId].initialCacheKey = generateUUID();
}
return {
addBid: function (bid) {
// create parent level object based on auction ID (in case there are concurrent auctions running) to store objects for that auction
if (!registry[bid.auctionId]) {
setupRegistrySlot(bid.auctionId);
}
registry[bid.auctionId].bidStorage.add(bid);
},
removeBid: function (bid) {
registry[bid.auctionId].bidStorage.delete(bid);
},
getBids: function (bid) {
return registry[bid.auctionId] && registry[bid.auctionId].bidStorage.values();
},
getQueueDispatcher: function (bid) {
return registry[bid.auctionId] && registry[bid.auctionId].queueDispatcher;
},
setupInitialCacheKey: function (bid) {
if (!registry[bid.auctionId]) {
registry[bid.auctionId] = {};
registry[bid.auctionId].initialCacheKey = generateUUID();
}
},
getInitialCacheKey: function (bid) {
return registry[bid.auctionId] && registry[bid.auctionId].initialCacheKey;
}
}
}
/**
* Creates a function that when called updates the bid queue and extends the running timer (when called subsequently).
* Once the time threshold for the queue (defined by queueSizeLimit) is reached, the queue will be flushed by calling the `firePrebidCacheCall` function.
* If there is a long enough time between calls (based on timeoutDration), the queue will automatically flush itself.
* @param {Number} timeoutDuration number of milliseconds to pass before timer expires and current bid queue is flushed
* @returns {Function}
*/
function createDispatcher(timeoutDuration) {
let timeout;
let counter = 1;
return function (auctionInstance, bidListArr, afterBidAdded, killQueue) {
const context = this;
var callbackFn = function () {
firePrebidCacheCall.call(context, auctionInstance, bidListArr, afterBidAdded);
};
clearTimeout(timeout);
if (!killQueue) {
// want to fire off the queue if either: size limit is reached or time has passed since last call to dispatcher
if (counter === queueSizeLimit) {
counter = 1;
callbackFn();
} else {
counter++;
timeout = setTimeout(callbackFn, timeoutDuration);
}
} else {
counter = 1;
}
};
}
function getPricePartForAdpodKey(bid) {
let pricePart
let prioritizeDeals = config.getConfig('adpod.prioritizeDeals');
if (prioritizeDeals && deepAccess(bid, 'video.dealTier')) {
const adpodDealPrefix = config.getConfig(`adpod.dealTier.${bid.bidderCode}.prefix`);
pricePart = (adpodDealPrefix) ? adpodDealPrefix + deepAccess(bid, 'video.dealTier') : deepAccess(bid, 'video.dealTier');
} else {
const granularity = getPriceGranularity(bid);
pricePart = getPriceByGranularity(granularity)(bid);
}
return pricePart
}
/**
* This function reads certain fields from the bid to generate a specific key used for caching the bid in Prebid Cache
* @param {Object} bid bid object to update
* @param {Boolean} brandCategoryExclusion value read from setConfig; influences whether category is required or not
*/
function attachPriceIndustryDurationKeyToBid(bid, brandCategoryExclusion) {
let initialCacheKey = bidCacheRegistry.getInitialCacheKey(bid);
let duration = deepAccess(bid, 'video.durationBucket');
const pricePart = getPricePartForAdpodKey(bid);
let pcd;
if (brandCategoryExclusion) {
let category = deepAccess(bid, 'meta.adServerCatId');
pcd = `${pricePart}_${category}_${duration}s`;
} else {
pcd = `${pricePart}_${duration}s`;
}
if (!bid.adserverTargeting) {
bid.adserverTargeting = {};
}
bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] = pcd;
bid.adserverTargeting[TARGETING_KEY_CACHE_ID] = initialCacheKey;
bid.videoCacheKey = initialCacheKey;
bid.customCacheKey = `${pcd}_${initialCacheKey}`;
}
/**
* Updates the running queue for the associated auction.
* Does a check to ensure the auction is still running; if it's not - the previously running queue is killed.
* @param {*} auctionInstance running context of the auction
* @param {Object} bidResponse bid object being added to queue
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
*/
function updateBidQueue(auctionInstance, bidResponse, afterBidAdded) {
let bidListIter = bidCacheRegistry.getBids(bidResponse);
if (bidListIter) {
let bidListArr = from(bidListIter);
let callDispatcher = bidCacheRegistry.getQueueDispatcher(bidResponse);
let killQueue = !!(auctionInstance.getAuctionStatus() !== AUCTION_IN_PROGRESS);
callDispatcher(auctionInstance, bidListArr, afterBidAdded, killQueue);
} else {
logWarn('Attempted to cache a bid from an unknown auction. Bid:', bidResponse);
}
}
/**
* Small helper function to remove bids from internal storage; normally b/c they're about to sent to Prebid Cache for processing.
* @param {Array[Object]} bidResponses list of bids to remove
*/
function removeBidsFromStorage(bidResponses) {
for (let i = 0; i < bidResponses.length; i++) {
bidCacheRegistry.removeBid(bidResponses[i]);
}
}
/**
* This function will send a list of bids to Prebid Cache. It also removes the same bids from the internal bidCacheRegistry
* to maintain which bids are in queue.
* If the bids are successfully cached, they will be added to the respective auction.
* @param {*} auctionInstance running context of the auction
* @param {Array[Object]} bidList list of bid objects that need to be sent to Prebid Cache
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
*/
function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) {
// remove entries now so other incoming bids won't accidentally have a stale version of the list while PBC is processing the current submitted list
removeBidsFromStorage(bidList);
store(bidList, function (error, cacheIds) {
if (error) {
logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`);
} else {
for (let i = 0; i < cacheIds.length; i++) {
// when uuid in response is empty string then the key already existed, so this bid wasn't cached
if (cacheIds[i].uuid !== '') {
addBidToAuction(auctionInstance, bidList[i]);
} else {
logInfo(`Detected a bid was not cached because the custom key was already registered. Attempted to use key: ${bidList[i].customCacheKey}. Bid was: `, bidList[i]);
}
afterBidAdded();
}
}
});
}
/**
* This is the main hook function to handle adpod bids; maintains the logic to temporarily hold bids in a queue in order to send bulk requests to Prebid Cache.
* @param {Function} fn reference to original function (used by hook logic)
* @param {*} auctionInstance running context of the auction
* @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function. If not adpod, returns to original function.
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
* @param {Object} videoConfig mediaTypes.video from the bid's adUnit
*/
export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, videoConfig) {
if (videoConfig && videoConfig.context === ADPOD) {
let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
let adServerCatId = deepAccess(bidResponse, 'meta.adServerCatId');
if (!adServerCatId && brandCategoryExclusion) {
logWarn('Detected a bid without meta.adServerCatId while setConfig({adpod.brandCategoryExclusion}) was enabled. This bid has been rejected:', bidResponse);
afterBidAdded();
} else {
if (config.getConfig('adpod.deferCaching') === false) {
bidCacheRegistry.addBid(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
} else {
// generate targeting keys for bid
bidCacheRegistry.setupInitialCacheKey(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
// add bid to auction
addBidToAuction(auctionInstance, bidResponse);
afterBidAdded();
}
}
} else {
fn.call(this, auctionInstance, bidResponse, afterBidAdded, videoConfig);
}
}
/**
* This hook function will review the adUnit setup and verify certain required values are present in any adpod adUnits.
* If the fields are missing or incorrectly setup, the adUnit is removed from the list.
* @param {Function} fn reference to original function (used by hook logic)
* @param {Array[Object]} adUnits list of adUnits to be evaluated
* @returns {Array[Object]} list of adUnits that passed the check
*/
export function checkAdUnitSetupHook(fn, adUnits) {
let goodAdUnits = adUnits.filter(adUnit => {
let mediaTypes = deepAccess(adUnit, 'mediaTypes');
let videoConfig = deepAccess(mediaTypes, 'video');
if (videoConfig && videoConfig.context === ADPOD) {
// run check to see if other mediaTypes are defined (ie multi-format); reject adUnit if so
if (Object.keys(mediaTypes).length > 1) {
logWarn(`Detected more than one mediaType in adUnitCode: ${adUnit.code} while attempting to define an 'adpod' video adUnit. 'adpod' adUnits cannot be mixed with other mediaTypes. This adUnit will be removed from the auction.`);
return false;
}
let errMsg = `Detected missing or incorrectly setup fields for an adpod adUnit. Please review the following fields of adUnitCode: ${adUnit.code}. This adUnit will be removed from the auction.`;
let playerSize = !!(
(
videoConfig.playerSize && (
isArrayOfNums(videoConfig.playerSize, 2) || (
isArray(videoConfig.playerSize) && videoConfig.playerSize.every(sz => isArrayOfNums(sz, 2))
)
)
) || (videoConfig.sizeConfig)
);
let adPodDurationSec = !!(videoConfig.adPodDurationSec && isNumber(videoConfig.adPodDurationSec) && videoConfig.adPodDurationSec > 0);
let durationRangeSec = !!(videoConfig.durationRangeSec && isArrayOfNums(videoConfig.durationRangeSec) && videoConfig.durationRangeSec.every(range => range > 0));
if (!playerSize || !adPodDurationSec || !durationRangeSec) {
errMsg += (!playerSize) ? '\nmediaTypes.video.playerSize' : '';
errMsg += (!adPodDurationSec) ? '\nmediaTypes.video.adPodDurationSec' : '';
errMsg += (!durationRangeSec) ? '\nmediaTypes.video.durationRangeSec' : '';
logWarn(errMsg);
return false;
}
}
return true;
});
adUnits = goodAdUnits;
fn.call(this, adUnits);
}
/**
* This check evaluates the incoming bid's `video.durationSeconds` field and tests it against specific logic depending on adUnit config. Summary of logic below:
* when adUnit.mediaTypes.video.requireExactDuration is true
* - only bids that exactly match those listed values are accepted (don't round at all).
* - populate the `bid.video.durationBucket` field with the matching duration value
* when adUnit.mediaTypes.video.requireExactDuration is false
* - round the duration to the next highest specified duration value based on adunit. If the duration is above a range within a set buffer, that bid falls down into that bucket.
* (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s)
* - if the bid is above the range of the listed durations (and outside the buffer), reject the bid
* - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids
* @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse
* @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory
* @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine
*/
function checkBidDuration(videoMediaType, bidResponse) {
const buffer = 2;
let bidDuration = deepAccess(bidResponse, 'video.durationSeconds');
let adUnitRanges = videoMediaType.durationRangeSec;
adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order
if (!videoMediaType.requireExactDuration) {
let max = Math.max(...adUnitRanges);
if (bidDuration <= (max + buffer)) {
let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration);
bidResponse.video.durationBucket = nextHighestRange;
} else {
logWarn(`Detected a bid with a duration value outside the accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Rejecting bid: `, bidResponse);
return false;
}
} else {
if (find(adUnitRanges, range => range === bidDuration)) {
bidResponse.video.durationBucket = bidDuration;
} else {
logWarn(`Detected a bid with a duration value not part of the list of accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Exact match durations must be used for this adUnit. Rejecting bid: `, bidResponse);
return false;
}
}
return true;
}
/**
* This hooked function evaluates an adpod bid and determines if the required fields are present.
* If it's found to not be an adpod bid, it will return to original function via hook logic
* @param {Function} fn reference to original function (used by hook logic)
* @param {Object} bid incoming bid object
* @param {Object} adUnit adUnit object of associated bid
* @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function
* @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function
* @returns {boolean} this return is only used for adpod bids
*/
export function checkVideoBidSetupHook(fn, bid, adUnit, videoMediaType, context) {
if (context === ADPOD) {
let result = true;
let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
if (brandCategoryExclusion && !deepAccess(bid, 'meta.primaryCatId')) {
result = false;
}
if (deepAccess(bid, 'video')) {
if (!deepAccess(bid, 'video.context') || bid.video.context !== ADPOD) {
result = false;
}
if (!deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) {
result = false;
} else {
let isBidGood = checkBidDuration(videoMediaType, bid);
if (!isBidGood) result = false;
}
}
if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) {
logError(`
This bid contains only vastXml and will not work when a prebid cache url is not specified.
Try enabling prebid cache with pbjs.setConfig({ cache: {url: "..."} });
`);
result = false;
};
fn.bail(result);
} else {
fn.call(this, bid, adUnit, videoMediaType, context);
}
}
/**
* This function reads the (optional) settings for the adpod as set from the setConfig()
* @param {Object} config contains the config settings for adpod module
*/
export function adpodSetConfig(config) {
if (config.bidQueueTimeDelay !== undefined) {
if (typeof config.bidQueueTimeDelay === 'number' && config.bidQueueTimeDelay > 0) {
queueTimeDelay = config.bidQueueTimeDelay;
} else {
logWarn(`Detected invalid value for adpod.bidQueueTimeDelay in setConfig; must be a positive number. Using default: ${queueTimeDelay}`)
}
}
if (config.bidQueueSizeLimit !== undefined) {
if (typeof config.bidQueueSizeLimit === 'number' && config.bidQueueSizeLimit > 0) {
queueSizeLimit = config.bidQueueSizeLimit;
} else {
logWarn(`Detected invalid value for adpod.bidQueueSizeLimit in setConfig; must be a positive number. Using default: ${queueSizeLimit}`)
}
}
}
config.getConfig('adpod', config => adpodSetConfig(config.adpod));
/**
* This function initializes the adpod module's hooks. This is called by the corresponding adserver video module.
*/
function initAdpodHooks() {
setupBeforeHookFnOnce(getHook('callPrebidCache'), callPrebidCacheHook);
setupBeforeHookFnOnce(checkAdUnitSetup, checkAdUnitSetupHook);
setupBeforeHookFnOnce(checkVideoBidSetup, checkVideoBidSetupHook);
}
initAdpodHooks()
/**
*
* @param {Array[Object]} bids list of 'winning' bids that need to be cached
* @param {Function} callback send the cached bids (or error) back to adserverVideoModule for further processing
}}
*/
export function callPrebidCacheAfterAuction(bids, callback) {
// will call PBC here and execute cb param to initialize player code
store(bids, function (error, cacheIds) {
if (error) {
callback(error, null);
} else {
let successfulCachedBids = [];
for (let i = 0; i < cacheIds.length; i++) {
if (cacheIds[i] !== '') {
successfulCachedBids.push(bids[i]);
}
}
callback(null, successfulCachedBids);
}
})
}
/**
* Compare function to be used in sorting long-form bids. This will compare bids on price per second.
* @param {Object} bid
*/
export function sortByPricePerSecond(a, b) {
if (a.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / a.video.durationBucket < b.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / b.video.durationBucket) {
return 1;
}
if (a.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / a.video.durationBucket > b.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / b.video.durationBucket) {
return -1;
}
return 0;
}
/**
* This function returns targeting keyvalue pairs for long-form adserver modules. Freewheel and GAM are currently supporting Prebid long-form
* @param {Object} options - Options for targeting.
* @param {Array<string>} options.codes - Array of ad unit codes.
* @param {function} options.callback - Callback function to handle the targeting key-value pairs.
* @returns {Object} Targeting key-value pairs for ad unit codes.
*/
export function getTargeting({ codes, callback } = {}) {
if (!callback) {
logError('No callback function was defined in the getTargeting call. Aborting getTargeting().');
return;
}
codes = codes || [];
const adPodAdUnits = getAdPodAdUnits(codes);
const bidsReceived = auctionManager.getBidsReceived();
const competiveExclusionEnabled = config.getConfig('adpod.brandCategoryExclusion');
const deferCachingSetting = config.getConfig('adpod.deferCaching');
const deferCachingEnabled = (typeof deferCachingSetting === 'boolean') ? deferCachingSetting : true;
let bids = getBidsForAdpod(bidsReceived, adPodAdUnits);
bids = (competiveExclusionEnabled || deferCachingEnabled) ? getExclusiveBids(bids) : bids;
let prioritizeDeals = config.getConfig('adpod.prioritizeDeals');
if (prioritizeDeals) {
let [otherBids, highPriorityDealBids] = bids.reduce((partitions, bid) => {
let bidDealTier = deepAccess(bid, 'video.dealTier');
let minDealTier = config.getConfig(`adpod.dealTier.${bid.bidderCode}.minDealTier`);
if (minDealTier && bidDealTier) {
if (bidDealTier >= minDealTier) {
partitions[1].push(bid)
} else {
partitions[0].push(bid)
}
} else if (bidDealTier) {
partitions[1].push(bid)
} else {
partitions[0].push(bid);
}
return partitions;
}, [[], []]);
highPriorityDealBids.sort(sortByPricePerSecond);
otherBids.sort(sortByPricePerSecond);
bids = highPriorityDealBids.concat(otherBids);
} else {
bids.sort(sortByPricePerSecond);
}
let targeting = {};
if (deferCachingEnabled === false) {
adPodAdUnits.forEach((adUnit) => {
let adPodTargeting = [];
let adPodDurationSeconds = deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');
bids
.filter((bid) => bid.adUnitCode === adUnit.code)
.forEach((bid, index, arr) => {
if (bid.video.durationBucket <= adPodDurationSeconds) {
adPodTargeting.push({
[TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
});
adPodDurationSeconds -= bid.video.durationBucket;
}
if (index === arr.length - 1 && adPodTargeting.length > 0) {
adPodTargeting.push({
[TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
});
}
});
targeting[adUnit.code] = adPodTargeting;
});
callback(null, targeting);
} else {
let bidsToCache = [];
adPodAdUnits.forEach((adUnit) => {
let adPodDurationSeconds = deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');
bids
.filter((bid) => bid.adUnitCode === adUnit.code)
.forEach((bid) => {
if (bid.video.durationBucket <= adPodDurationSeconds) {
bidsToCache.push(bid);
adPodDurationSeconds -= bid.video.durationBucket;
}
});
});
callPrebidCacheAfterAuction(bidsToCache, function (error, bidsSuccessfullyCached) {
if (error) {
callback(error, null);
} else {
let groupedBids = groupBy(bidsSuccessfullyCached, 'adUnitCode');
Object.keys(groupedBids).forEach((adUnitCode) => {
let adPodTargeting = [];
groupedBids[adUnitCode].forEach((bid, index, arr) => {
adPodTargeting.push({
[TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
});
if (index === arr.length - 1 && adPodTargeting.length > 0) {
adPodTargeting.push({
[TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
});
}
});
targeting[adUnitCode] = adPodTargeting;
});
callback(null, targeting);
}
});
}
return targeting;
}
/**
* This function returns the adunit of mediaType adpod
* @param {Array} codes adUnitCodes
* @returns {Array[Object]} adunits of mediaType adpod
*/
function getAdPodAdUnits(codes) {
return auctionManager.getAdUnits()
.filter((adUnit) => deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD)
.filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true);
}
/**
* This function will create compare function to sort on object property
* @param {string} property
* @returns {function} compare function to be used in sorting
*/
function compareOn(property) {
return function compare(a, b) {
if (a[property] < b[property]) {
return 1;
}
if (a[property] > b[property]) {
return -1;
}
return 0;
}
}
/**
* This function removes bids of same category. It will be used when competitive exclusion is enabled.
* @param {Array[Object]} bidsReceived
* @returns {Array[Object]} unique category bids
*/
function getExclusiveBids(bidsReceived) {
let bids = bidsReceived
.map((bid) => Object.assign({}, bid, { [TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] }));
bids = groupBy(bids, TARGETING_KEY_PB_CAT_DUR);
let filteredBids = [];
Object.keys(bids).forEach((targetingKey) => {
bids[targetingKey].sort(compareOn('responseTimestamp'));
filteredBids.push(bids[targetingKey][0]);
});
return filteredBids;
}
/**
* This function returns bids for adpod adunits
* @param {Array[Object]} bidsReceived
* @param {Array[Object]} adPodAdUnits
* @returns {Array[Object]} bids of mediaType adpod
*/
function getBidsForAdpod(bidsReceived, adPodAdUnits) {
let adUnitCodes = adPodAdUnits.map((adUnit) => adUnit.code);
return bidsReceived
.filter((bid) => adUnitCodes.indexOf(bid.adUnitCode) != -1 && (bid.video && bid.video.context === ADPOD))
}
const sharedMethods = {
TARGETING_KEY_PB_CAT_DUR: TARGETING_KEY_PB_CAT_DUR,
TARGETING_KEY_CACHE_ID: TARGETING_KEY_CACHE_ID,
'getTargeting': getTargeting
}
Object.freeze(sharedMethods);
module('adpod', function shareAdpodUtilities(...args) {
if (!isPlainObject(args[0])) {
logError('Adpod module needs plain object to share methods with submodule');
return;
}
function addMethods(object, func) {
for (let name in func) {
object[name] = func[name];
}
}
addMethods(args[0], sharedMethods);
});