Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for video stream context #1483

Merged
merged 14 commits into from
Sep 15, 2017
15 changes: 10 additions & 5 deletions modules/appnexusAstBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NATIVE, VIDEO } from 'src/mediaTypes';

const BIDDER_CODE = 'appnexusAst';
const URL = '//ib.adnxs.com/ut/v3/prebid';
const SUPPORTED_AD_TYPES = ['banner', 'video', 'video-outstream', 'native'];
const SUPPORTED_AD_TYPES = ['banner', 'video', 'native'];
const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration',
'startdelay', 'skippable', 'playback_method', 'frameworks'];
const USER_PARAMS = ['age', 'external_uid', 'segments', 'gender', 'dnt', 'language'];
Expand Down Expand Up @@ -218,6 +218,7 @@ function newBid(serverBid, rtbBid) {

return bid;
}

function bidToTag(bid) {
const tag = {};
tag.sizes = transformSizes(bid.sizes);
Expand Down Expand Up @@ -289,7 +290,13 @@ function bidToTag(bid) {
}
}

if (bid.mediaType === 'video') { tag.require_asset_url = true; }
const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video');
const context = utils.deepAccess(bid, 'mediaTypes.video.context');

if (bid.mediaType === 'video' || (videoMediaType && context !== 'outstream')) {
tag.require_asset_url = true;
}

if (bid.params.video) {
tag.video = {};
// place any valid video params on the tag
Expand Down Expand Up @@ -356,9 +363,7 @@ function handleOutstreamRendererEvents(bid, id, eventName) {

function parseMediaType(rtbBid) {
const adType = rtbBid.ad_type;
if (rtbBid.renderer_url) {
return 'video-outstream';
} else if (adType === 'video') {
if (adType === 'video') {
return 'video';
} else if (adType === 'native') {
return 'native';
Expand Down
10 changes: 9 additions & 1 deletion modules/unrulyBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ function UnrulyAdapter() {
return
}

const videoMediaType = utils.deepAccess(bidRequestBids[0], 'mediaTypes.video')
const context = utils.deepAccess(bidRequestBids[0], 'mediaTypes.video.context')
if (videoMediaType && context !== 'outstream') {
return
}

const payload = {
bidRequests: bidRequestBids
}
Expand All @@ -106,6 +112,8 @@ function UnrulyAdapter() {
return adapter
}

adaptermanager.registerBidAdapter(new UnrulyAdapter(), 'unruly')
adaptermanager.registerBidAdapter(new UnrulyAdapter(), 'unruly', {
supportedMediaTypes: ['video']
});

module.exports = UnrulyAdapter
19 changes: 16 additions & 3 deletions src/adaptermanager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @module adaptermanger */

import { flatten, getBidderCodes, shuffle } from './utils';
import { flatten, getBidderCodes, getDefinedParams, shuffle } from './utils';
import { mapSizes } from './sizeMapping';
import { processNativeAdUnitParams, nativeAdapters } from './native';
import { StorageManager, pbjsSyncsKey } from './storagemanager';
Expand Down Expand Up @@ -48,10 +48,23 @@ function getBids({bidderCode, requestId, bidderRequestId, adUnits}) {
});
}

if (adUnit.mediaTypes) {
if (utils.isValidMediaTypes(adUnit.mediaTypes)) {
bid = Object.assign({}, bid, { mediaTypes: adUnit.mediaTypes });
} else {
utils.logError(
`mediaTypes is not correctly configured for adunit ${adUnit.code}`
);
}
}

bid = Object.assign({}, bid, getDefinedParams(adUnit, [
'mediaType',
'renderer'
]));

return Object.assign({}, bid, {
placementCode: adUnit.code,
mediaType: adUnit.mediaType,
renderer: adUnit.renderer,
transactionId: adUnit.transactionId,
sizes: sizes,
bidId: bid.bid_id || utils.getUniqueIdentifierStr(),
Expand Down
5 changes: 3 additions & 2 deletions src/bidmanager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { uniques, flatten, adUnitsFilter, getBidderRequest } from './utils';
import { getPriceBucketString } from './cpmBucketManager';
import { NATIVE_KEYS, nativeBidIsValid } from './native';
import { isValidVideoBid } from './video';
import { getCacheUrl, store } from './videoCache';
import { Renderer } from 'src/Renderer';
import { config } from 'src/config';
Expand Down Expand Up @@ -113,8 +114,8 @@ exports.addBidResponse = function (adUnitCode, bid) {
utils.logError(errorMessage('Native bid missing some required properties.'));
return false;
}
if (bid.mediaType === 'video' && !(bid.vastUrl || bid.vastXml)) {
utils.logError(errorMessage(`Video bid has no vastUrl or vastXml property.`));
if (bid.mediaType === 'video' && !isValidVideoBid(bid)) {
utils.logError(errorMessage(`Video bid does not have required vastUrl or renderer property`));
return false;
}
if (bid.mediaType === 'banner' && !validBidSize(bid)) {
Expand Down
42 changes: 42 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -720,3 +720,45 @@ export function deepAccess(obj, path) {
}
return obj;
}

/**
* Build an object consisting of only defined parameters to avoid creating an
* object with defined keys and undefined values.
* @param {object} object The object to pick defined params out of
* @param {string[]} params An array of strings representing properties to look for in the object
* @returns {object} An object containing all the specified values that are defined
*/
export function getDefinedParams(object, params) {
return params
.filter(param => object[param])
.reduce((bid, param) => Object.assign(bid, { [param]: object[param] }), {});
}

/**
* @typedef {Object} MediaTypes
* @property {Object} banner banner configuration
* @property {Object} native native configuration
* @property {Object} video video configuration
*/

/**
* Validates an adunit's `mediaTypes` parameter
* @param {MediaTypes} mediaTypes mediaTypes parameter to validate
* @return {boolean} If object is valid
*/
export function isValidMediaTypes(mediaTypes) {
const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video'];
const SUPPORTED_STREAM_TYPES = ['instream', 'outstream'];

const types = Object.keys(mediaTypes);

if (!types.every(type => SUPPORTED_MEDIA_TYPES.includes(type))) {
return false;
}

if (mediaTypes.video && mediaTypes.video.context) {
return SUPPORTED_STREAM_TYPES.includes(mediaTypes.video.context);
}

return true;
}
40 changes: 38 additions & 2 deletions src/video.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import { videoAdapters } from './adaptermanager';
import { getBidRequest, deepAccess } from './utils';

const VIDEO_MEDIA_TYPE = 'video';
const OUTSTREAM = 'outstream';

/**
* Helper functions for working with video-enabled adUnits
*/
export const videoAdUnit = adUnit => adUnit.mediaType === 'video';
export const videoAdUnit = adUnit => adUnit.mediaType === VIDEO_MEDIA_TYPE;
const nonVideoBidder = bid => !videoAdapters.includes(bid.bidder);
export const hasNonVideoBidder = adUnit => adUnit.bids.filter(nonVideoBidder).length;
export const hasNonVideoBidder = adUnit =>
adUnit.bids.filter(nonVideoBidder).length;

/**
* @typedef {object} VideoBid
* @property {string} adId id of the bid
*/

/**
* Validate that the assets required for video context are present on the bid
* @param {VideoBid} bid video bid to validate
* @return {boolean} If object is valid
*/
export function isValidVideoBid(bid) {
const bidRequest = getBidRequest(bid.adId);

const videoMediaType =
bidRequest && deepAccess(bidRequest, 'mediaTypes.video');
const context = videoMediaType && deepAccess(videoMediaType, 'context');

// if context not defined assume default 'instream' for video bids
// instream bids require a vast url or vast xml content
if (!bidRequest || (videoMediaType && context !== OUTSTREAM)) {
return !!(bid.vastUrl || bid.vastXml);
}

// outstream bids require a renderer on the bid or pub-defined on adunit
if (context === OUTSTREAM) {
return !!(bid.renderer || bidRequest.renderer);
}

return true;
}
24 changes: 24 additions & 0 deletions test/spec/bidmanager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,5 +597,29 @@ describe('bidmanager.js', function () {

utils.getBidderRequest.restore();
});

it('requires a renderer on outstream bids', () => {
sinon.stub(utils, 'getBidRequest', () => ({
bidder: 'appnexusAst',
mediaTypes: {
video: {context: 'outstream'}
},
}));

const bid = Object.assign({},
bidfactory.createBid(1),
{
bidderCode: 'appnexusAst',
mediaType: 'video',
renderer: {render: () => true, url: 'render.js'},
}
);

const bidsRecCount = $$PREBID_GLOBAL$$._bidsReceived.length;
bidmanager.addBidResponse('adUnit-code', bid);
assert.equal(bidsRecCount + 1, $$PREBID_GLOBAL$$._bidsReceived.length);

utils.getBidRequest.restore();
});
});
});
2 changes: 1 addition & 1 deletion test/spec/modules/unrulyBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('UnrulyAdapter', () => {
'placementId': '5768085'
},
'placementCode': placementCode,
'mediaType': 'video',
'mediaTypes': { video: { context: 'outstream' } },
'transactionId': '62890707-3770-497c-a3b8-d905a2d0cb98',
'sizes': [
640,
Expand Down
19 changes: 19 additions & 0 deletions test/spec/utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,23 @@ describe('Utils', function () {
assert.equal(value, undefined);
});
});

describe('getDefinedParams', () => {
it('builds an object consisting of defined params', () => {
const adUnit = {
mediaType: 'video',
comeWithMe: 'ifuwant2live',
notNeeded: 'do not include',
};

const builtObject = utils.getDefinedParams(adUnit, [
'mediaType', 'comeWithMe'
]);

assert.deepEqual(builtObject, {
mediaType: 'video',
comeWithMe: 'ifuwant2live',
});
});
});
});
67 changes: 67 additions & 0 deletions test/spec/video_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { isValidVideoBid } from 'src/video';
const utils = require('src/utils');

describe('video.js', () => {
afterEach(() => {
utils.getBidRequest.restore();
});

it('validates valid instream bids', () => {
sinon.stub(utils, 'getBidRequest', () => ({
bidder: 'appnexusAst',
mediaTypes: {
video: { context: 'instream' },
},
}));

const valid = isValidVideoBid({
vastUrl: 'http://www.example.com/vastUrl'
});

expect(valid).to.be(true);
});

it('catches invalid instream bids', () => {
sinon.stub(utils, 'getBidRequest', () => ({
bidder: 'appnexusAst',
mediaTypes: {
video: { context: 'instream' },
},
}));

const valid = isValidVideoBid({});

expect(valid).to.be(false);
});

it('validates valid outstream bids', () => {
sinon.stub(utils, 'getBidRequest', () => ({
bidder: 'appnexusAst',
mediaTypes: {
video: { context: 'outstream' },
},
}));

const valid = isValidVideoBid({
renderer: {
url: 'render.url',
render: () => true,
}
});

expect(valid).to.be(true);
});

it('catches invalid outstream bids', () => {
sinon.stub(utils, 'getBidRequest', () => ({
bidder: 'appnexusAst',
mediaTypes: {
video: { context: 'outstream' },
},
}));

const valid = isValidVideoBid({});

expect(valid).to.be(false);
});
});