Skip to content

Commit

Permalink
Add support for video stream context (#1483)
Browse files Browse the repository at this point in the history
* Add support for video stream context

* Define adapter as supporting video

* Use mediaTypes param to specify context

* Use utils.deepAccess

* Check for outstream bids

* Add JSDoc and validation

* Rename functions and add unit test

* Update property name

* Update stubs to new sinon stub syntax

* Only check context when mediaTypes.video was defined

* Retain video-outstream compatibility

* Revert to Sinon 1 syntax

* Server and bid response ad type for any stream type is always 'video'

* Update to address code review
  • Loading branch information
matthewlane authored and Matt Kendall committed Sep 15, 2017
1 parent 8e0ce69 commit fcf8d18
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 14 deletions.
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 @@ -120,8 +121,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 @@ -622,5 +622,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);
});
});

0 comments on commit fcf8d18

Please sign in to comment.