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)))
+ });
+ }
});
}