diff --git a/integrationExamples/videoModule/adPlayerPro/localVideoCache.html b/integrationExamples/videoModule/adPlayerPro/localVideoCache.html new file mode 100644 index 00000000000..88a6b02c6f3 --- /dev/null +++ b/integrationExamples/videoModule/adPlayerPro/localVideoCache.html @@ -0,0 +1,163 @@ + + + + + + + + AdPlayer.Pro with Local Cache + + + + + + +

AdPlayer.Pro with Local Cache

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/jwplayer/localVideoCache.html b/integrationExamples/videoModule/jwplayer/localVideoCache.html new file mode 100644 index 00000000000..a36d85975f3 --- /dev/null +++ b/integrationExamples/videoModule/jwplayer/localVideoCache.html @@ -0,0 +1,132 @@ + + + + + + + JW Player with Local Cache + + + + + + +

JW Player with Local cache

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/videojs/localVideoCache.html b/integrationExamples/videoModule/videojs/localVideoCache.html new file mode 100644 index 00000000000..c28d6fe4fb8 --- /dev/null +++ b/integrationExamples/videoModule/videojs/localVideoCache.html @@ -0,0 +1,147 @@ + + + + + + + + + + --> + + + + VideoJS with GAM Ad Server Mediation + + + + + + + +

VideoJS with GAM Ad Server Mediation

+
Div-1: Player placeholder div
+ + + + diff --git a/modules/jwplayerVideoProvider.js b/modules/jwplayerVideoProvider.js index 54de1949e6f..977a00961e4 100644 --- a/modules/jwplayerVideoProvider.js +++ b/modules/jwplayerVideoProvider.js @@ -190,7 +190,11 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba return; } - player.playAd(adTagUrl || options.adXml, options); + if (adTagUrl) { + player.playAd(adTagUrl, options); + } else { + player.loadAdXml(options.adXml, options); + } } function onEvent(externalEventName, callback, basePayload) { diff --git a/modules/videoModule/index.js b/modules/videoModule/index.js index c84d98a6d5f..ef68684b4af 100644 --- a/modules/videoModule/index.js +++ b/modules/videoModule/index.js @@ -24,6 +24,7 @@ import { getExternalVideoEventName, getExternalVideoEventPayload } from '../../l import {VIDEO} from '../../src/mediaTypes.js'; import {auctionManager} from '../../src/auctionManager.js'; import {doRender} from '../../src/adRendering.js'; +import { getLocalCachedBidWithGam } from '../../src/videoCache.js'; const allVideoEvents = Object.keys(videoEvents).map(eventKey => videoEvents[eventKey]); events.addEvents(allVideoEvents.concat([AUCTION_AD_LOAD_ATTEMPT, AUCTION_AD_LOAD_QUEUED, AUCTION_AD_LOAD_ABORT, BID_IMPRESSION, BID_ERROR]).map(getExternalVideoEventName)); @@ -217,6 +218,12 @@ export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvent } if (adUrl) { + if (config.getConfig('cache.useLocal')) { + getLocalCachedBidWithGam(adUrl).then((vastXml) => { + loadAdTag(null, divId, {...options, adXml: vastXml}); + }) + return; + } loadAdTag(adUrl, divId, options); return; } diff --git a/modules/videojsVideoProvider.js b/modules/videojsVideoProvider.js index 0be4c6feede..383da63e905 100644 --- a/modules/videojsVideoProvider.js +++ b/modules/videojsVideoProvider.js @@ -201,13 +201,17 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call // Plugins to integrate: https://github.com/googleads/videojs-ima function setAdTagUrl(adTagUrl, options) { - if (!player.ima || !adTagUrl) { + if (!player.ima) { return; } // The VideoJS IMA plugin version 1.11.0 will throw when the ad is empty. try { - player.ima.changeAdTag(adTagUrl); + if (!adTagUrl && options.adXml) { + player.ima.controller.settings.adsResponse = options.adXml; + } else { + player.ima.changeAdTag(adTagUrl); + } player.ima.requestAds(); } catch (e) { /* diff --git a/src/auction.js b/src/auction.js index 759397275d5..cc91f67e200 100644 --- a/src/auction.js +++ b/src/auction.js @@ -81,7 +81,7 @@ import { } from './utils.js'; import {getPriceBucketString} from './cpmBucketManager.js'; import {getNativeTargeting, isNativeResponse, setNativeResponseProperties} from './native.js'; -import {batchAndStore} from './videoCache.js'; +import {batchAndStore, storeLocally} from './videoCache.js'; import {Renderer} from './Renderer.js'; import {config} from './config.js'; import {userSync} from './userSync.js'; @@ -566,15 +566,23 @@ function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = au }), 'video'); const context = videoMediaType && deepAccess(videoMediaType, 'context'); const useCacheKey = videoMediaType && deepAccess(videoMediaType, 'useCacheKey'); - - if (config.getConfig('cache.url') && (useCacheKey || context !== OUTSTREAM)) { - if (!bidResponse.videoCacheKey || config.getConfig('cache.ignoreBidderCacheKey')) { + const { + useLocal, + url: cacheUrl, + ignoreBidderCacheKey + } = config.getConfig('cache') || {}; + + if (cacheUrl && (useCacheKey || context !== OUTSTREAM)) { + if (!bidResponse.videoCacheKey || ignoreBidderCacheKey) { addBid = false; callPrebidCache(auctionInstance, bidResponse, afterBidAdded, videoMediaType); } else if (!bidResponse.vastUrl) { logError('videoCacheKey specified but not required vastUrl for video bid'); addBid = false; } + } else if (useLocal) { + // stores video bid vast as local blob in the browser + storeLocally(bidResponse); } if (addBid) { addBidToAuction(auctionInstance, bidResponse); diff --git a/src/video.js b/src/video.js index 9be9adec4c5..8d2c05cc3b0 100644 --- a/src/video.js +++ b/src/video.js @@ -120,10 +120,12 @@ export function isValidVideoBid(bid, {index = auctionManager.index} = {}) { export const checkVideoBidSetup = hook('sync', function(bid, adUnit, videoMediaType, context, useCacheKey) { if (videoMediaType && (useCacheKey || context !== OUTSTREAM)) { // xml-only video bids require a prebid cache url - if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) { + const { url, useLocal } = config.getConfig('cache') || {}; + if ((!url && !useLocal) && 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 $$PREBID_GLOBAL$$.setConfig({ cache: {url: "..."} }); + Try enabling either prebid cache with $$PREBID_GLOBAL$$.setConfig({ cache: {url: "..."} }); + or local cache with $$PREBID_GLOBAL$$.setConfig({ cache: { useLocal: true }}); `); return false; } diff --git a/src/videoCache.js b/src/videoCache.js index cf39c1c9452..e493bee2195 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -62,6 +62,10 @@ function wrapURI(uri, impTrackerURLs) { `; } +export const vastsLocalCache = new Map(); + +export const LOCAL_CACHE_MOCK_URL = 'https://local.prebid.org/cache?bidder='; + /** * Wraps a bid in the format expected by the prebid-server endpoints, or returns null if * the bid can't be converted cleanly. @@ -72,7 +76,7 @@ function wrapURI(uri, impTrackerURLs) { * @return {Object|null} - The payload to be sent to the prebid-server endpoints, or null if the bid can't be converted cleanly. */ function toStorageRequest(bid, {index = auctionManager.index} = {}) { - const vastValue = bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl); + const vastValue = getVastValue(bid); const auction = index.getAuction(bid); const ttlWithBuffer = Number(bid.ttl) + ttlBufferInSeconds; let payload = { @@ -140,6 +144,10 @@ function shimStorageCallback(done) { } } +function getVastValue(bid) { + return bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl); +}; + /** * If the given bid is for a Video ad, generate a unique ID and cache it somewhere server-side. * @@ -162,6 +170,36 @@ export function getCacheUrl(id) { return `${config.getConfig('cache.url')}?uuid=${id}`; } +export const storeLocally = (bid) => { + const vastValue = getVastValue(bid); + const dataUri = 'data:text/xml;base64,' + btoa(vastValue); + bid.vastUrl = dataUri; + //@todo: think of wrapping it with [if (adServer)] + vastsLocalCache.set(getLocalCacheBidId(bid), dataUri); +}; + +export async function getLocalCachedBidWithGam(adTagUrl) { + const gamAdTagUrl = new URL(adTagUrl); + const custParams = new URLSearchParams(gamAdTagUrl.searchParams.get('cust_params')); + const hb_bidder = custParams.get('hb_bidder'); + const hb_adid = custParams.get('hb_adid'); + const response = await fetch(gamAdTagUrl); + + if (!response.ok) { + logError('Unable to fetch valid response from Google Ad Manager'); + return; + } + + const gamVastWrapper = await response.text(); + const bidVastDataUri = vastsLocalCache.get(`${hb_bidder}_${hb_adid}`); + const mockUrl = LOCAL_CACHE_MOCK_URL + hb_bidder; + const combinedVast = gamVastWrapper.replace(mockUrl, bidVastDataUri); + + return combinedVast; +} + +const getLocalCacheBidId = (bid) => `${bid.bidderCode}_${bid.adId}`; + export const _internal = { store } @@ -194,15 +232,23 @@ export function storeBatch(batch) { }); }; -let batchSize, batchTimeout; +let batchSize, batchTimeout, cleanupHandler; if (FEATURES.VIDEO) { - config.getConfig('cache', (cacheConfig) => { - batchSize = typeof cacheConfig.cache.batchSize === 'number' && cacheConfig.cache.batchSize > 0 - ? cacheConfig.cache.batchSize + config.getConfig('cache', ({cache}) => { + batchSize = typeof cache.batchSize === 'number' && cache.batchSize > 0 + ? cache.batchSize : 1; - batchTimeout = typeof cacheConfig.cache.batchTimeout === 'number' && cacheConfig.cache.batchTimeout > 0 - ? cacheConfig.cache.batchTimeout + batchTimeout = typeof cache.batchTimeout === 'number' && cache.batchTimeout > 0 + ? cache.batchTimeout : 0; + + // removing blobs that are not going to be used + if (cache.useLocal && !cleanupHandler) { + cleanupHandler = auctionManager.onExpiry((auction) => { + auction.getBidsReceived() + .forEach((bid) => vastsLocalCache.delete(getLocalCacheBidId(bid))) + }); + } }); }